Thursday, May 1, 2008

Sample code for adding/deleting MOSS alerts

Please somebody tell me there's a better way to delete an alert. I'm too lazy to spend more time on this...

Create an alert...

   1: try
   2: {
   3:     string[] alertUsers = web.Alerts.GetUniqueUsers();
   4:     Guid alertGuid = web.Alerts.Add(
   5:         web.Lists[_documentLibraryName], 
   6:         SPEventType.Add, 
   7:         SPAlertFrequency.Immediate);
   8:     SPAlert alert = web.Alerts[alertGuid];
   9:     alert.User = web.SiteUsers[roleName];
  10:     alert.Update();
  11: }
  12: catch (Exception e)
  13: {
  14:     throw new Exception("Error creating document library notification for company.", e);
  15: }


Delete an alert...





   1: string[] usersWithAlerts = web.Alerts.GetUniqueUsers();
   2: //check to see if there is an alert configured for this role
   3: if (Array.IndexOf(userName) >= 0)
   4: {
   5:     SPList docList = web.Lists[_documentLibraryName];
   6:     for( int i = web.Alerts.Count -1; i >= 0; i--)
   7:     {
   8:         SPAlert alert = web.Alerts[i];
   9:         if (alert.List = docList)
  10:         {
  11:             if (alert.User.Name == roleUserName)
  12:             {
  13:                 web.Alerts.Delete(alert.ID);
  14:             }
  15:         }
  16:     }
  17: }


Tuesday, April 8, 2008

Super Simple Feature for Hiding the Search Scope DropDown

So I need to hide the scope drop down that shows up next to the search box in my SharePoint MasterPage. 

Me:  "That's gotta be simple...  Just open up SharePoint designer and set some property on the search control." 

SharePoint: "Silly little boy!  Say hello to my little friend...err... DelegateControl!"

<Enter Jesse into world of SharePoint delegate controls>

Foolishness aside, the concept behind the DelegateControl is actually pretty useful, and because of it I was able to roll a very straightforward feature permitting me to hide the scope drop down in my MasterPage.  And I didn't even have to write one line of code... a true ode to developer sloth.

The feature....

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
   3: <Feature  Id="085B7E09-1D3E-41a7-9FEE-0C88F4394920"
   4:           Title="Custom Basic Search Control Feature"
   5:           Description="A feature for a search control that hides the Scope drop-down."
   6:           DefaultResourceFile="spscore"
   7:           Version="1.0.0.0"
   8:           Scope="WebApplication"
   9:           xmlns="http://schemas.microsoft.com/sharepoint/">
  10:     <ElementManifests>
  11:         <ElementManifest Location="searcharea.xml"/>
  12:     </ElementManifests>
  13: </Feature>

 


SearchArea.xml...



   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   3:     <Control 
   4:         Id="CustomSmallSearchInputBox" 
   5:         Sequence="1"
   6:         ControlClass="Microsoft.SharePoint.Portal.WebControls.SearchBoxEx" ControlAssembly="Microsoft.SharePoint.Portal, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">
   7:     <Property Name="GoImageUrl">/_layouts/images/gosearch.gif</Property>
   8:     <Property Name="GoImageUrlRTL">/_layouts/images/goRTL.gif</Property>
   9:     <Property Name="GoImageActiveUrl">/_layouts/images/gosearch.gif</Property>
  10:     <Property Name="GoImageActiveUrlRTL">/_layouts/images/goRTL.gif</Property>
  11:     <Property Name="DropDownMode">HideScopeDD</Property>
  12:     <Property Name="SearchResultPageURL">/_layouts/osssearchresults.aspx</Property>
  13:     <Property Name="ScopeDisplayGroupName"></Property>
  14:     <Property Name="FrameType">None</Property>
  15:     </Control>
  16: </Elements>

Two important things to note here... the Id of the control (CustomSmallSearchInputBox) and the DropDownMode (there are actually a bunch of different values you can set here).  So, install your feature, reference the CustomSmallSearchInputBox in your MasterPage and you're ret' to go:



   1: <asp:ContentPlaceHolder id="PlaceHolderSearchArea" runat="server">
   2:       <SharePoint:DelegateControl runat="server" ControlId="CustomSmallSearchInputBox"/>
   3: </asp:ContentPlaceHolder>

Theoretically speaking, you could roll your feature with the same ID as the SharePoint feature (SmallSearchInputBox), set the Sequence attribute to something super low, install your feature, and have your customized version start showing up throughout the site.  I myself prefer to keep my changes more modular.

WebPart File is not Overwritten After First Solution Deployment

We setup a nightly build process for a recent client which automagically builds and deploys a SharePoint solution.  Soon after it was implemented we realized that our (one) WebPart wasn't updating in the WebPart gallery of our target site.  In other words, the developers were checking in changes to the .webpart file in TFS, that .webpart file was being deployed to the target site, the feature was being reactivated without issue, but the .webpart file in the WebPart Gallery List remained unchanged. 

I suspect that this may have something to do with how we were doing successive deployments (using deploy/install instead of upgrade?), but I figured I'd come up with a solution to the problem within the feature itself.  Pretty simple, actually.  I wrote a piece of feature receiver code that, on feature deactivating, programmatically removes the WebPart file from the Web Part Gallery.

The Feature:

   1: <Feature
   2:   Id="7CAB976B-018D-4ec8-B1A2-354ED7645795"
   3:   Title="AIS Sample Hello World WebPart"
   4:   Description="Basic WebPart example."
   5:   Hidden="FALSE"
   6:   Scope="Site"
   7:   ImageUrl="actionsettings.gif"
   8:   ReceiverAssembly="AIS.SharePoint.Utilities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a961f5844c0e1ceb"
   9:   ReceiverClass="AIS.SharePoint.Utilities.EventReceivers.AisWebPartFeaturReceiver"
  10:   xmlns="http://schemas.microsoft.com/sharepoint/">
  11:   <ElementManifests>
  12:     <ElementManifest Location="ProvisionedFiles.xml"/>
  13:     <ElementFile Location="HelloWorld.WebPart" />
  14:   </ElementManifests>
  15:     <Properties>
  16:         <Property Key="WebPartFileName" Value="HelloWorld.WebPart"/>
  17:     </Properties>  
  18: </Feature>


The Code:



   1: public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
   2: {
   3:     try
   4:     {
   5:         SPFeatureProperty prop = properties.Feature.Properties["WebPartFileName"];
   6:         if (prop != null)
   7:         {
   8:             string filenameUpper = prop.Value.ToUpper();
   9:             SPWeb web = ((SPSite)properties.Feature.Parent).OpenWeb();
  10:             SPList webPartList = web.Lists["Web Part Gallery"];
  11:             int id = -1;
  12:             foreach (SPItem item in webPartList.Items)
  13:             {
  14:                 if (item["Name"].ToString().ToUpper() == filenameUpper)
  15:                 {
  16:                     id = item.ID;
  17:                     break;
  18:                 }
  19:             }
  20:  
  21:             if (id >= 0)
  22:             {
  23:                 webPartList.Items.DeleteItemById(id);
  24:                 System.Diagnostics.EventLog.WriteEntry(this.ToString(), "Successfully removed webpart file from webpart gallery.", System.Diagnostics.EventLogEntryType.Information);
  25:             }
  26:         }
  27:  
  28:     }
  29:     catch (Exception e)
  30:     {
  31:         System.Diagnostics.EventLog.WriteEntry(this.ToString(), "An error occurred during feature deactivation." + Environment.NewLine + e.ToString(), System.Diagnostics.EventLogEntryType.Error);
  32:     }
  33:  
  34: }

I obviously could have been a little more fancy with how I queried the list, etc, but I think this gets the point across...  If anyone has any input on items in a list not updating, please feel free to chime in.  I've seen similar behavior when deploying stylesheets, masterpages, etc, as part of a feature.


Hope this helps somebody!


UPDATE:  Props to my buddy Oskar for pointing me to the Windows Live Writer Code Snippet Plug-In.  And, uh... Do people still say 'Props'???

Saturday, March 8, 2008

SharePoint Web.config Debug Settings

I always seem to forget the exact changes needed in your web.config to get some REAL error information in your browser when debugging SharePoint (other than setting customErrors = false). 

<configuration>
    <SharePoint>
        <SafeMode CallStack="true" />
    </SharePoint> 
    <system.web>
        <customErrors mode="Off" />
        <compilation debug="true" />
    </system.web>
</configuration>



There’s also this posting which offers a variety of other approaches, but I’m yet to have much success with many of them (although I admittedly haven't spent much time trying).

Integrating an ASP.NET Application into our SharePoint Portal

This was actually a conversion and integration effort.  Our client had a rather old and outdated MS Access application that they wanted to integrate into their soon-to-be-deployed SharePoint portal.  My good friend and coworker, Yuriy Shvadskiy, took on the tremendous effort of first converting the Access database to Sql Server 2005 and also designed/implemented the Asp.Net front-end.  Perhaps I'll try to get him to write something up about this effort... the final product was a great conglomeration of skill and technology.

After the Asp.Net app was complete we formulated a plan for MOSS integration (with some guidance from our trusty SharePoint Guru).  As many of us know, there are a couple ways to go about this.  I think the most well known and popular is probably ye olde _layouts directory.  Pop your assemblies in the GAC (or web app bin), drop your aspx pages in 12\Template\Layouts, and... whalah - application integrated (yes, I know I'm probably missing some steps here).  My quarrel with this approach is that we end up losing a fair amount of the functionality provided by SharePoint, most importantly security.

In the end we went with an approach that Vishwas outlines in one if his whitepapers (the name and location of which escapes me now): Convert the Asp.Net front end into a series of custom publishing page layouts and deploy the page layouts to our target site.  Here's an outline of the steps we took:

  1. Convert each Asp.Net page into one or more Asp.Net User Controls.
  2. Deploy the controls to the ControlTemplates directory.
  3. For each page in the Asp.Net app, create a custom page layout containing its respective User Control(s).
  4. Deploy the page layouts to the target site, and create a single page instance for each page layout.

Pretty simple, really.  Our final product was a SharePoint solution consisting of two features:  one for the system master page and file dependencies (stylesheets, etc) and another for the custom  page layouts.  The solution also packaged the system assemblies, web services, and custom User Controls. But, as we all know, the devil is in the details.  Creating a robust and usable product required us to do more than just deploy the layouts.  I'll try to supply a couple of the points and snippets that I found to be the most useful.  Please let me know if there's something you think that I should cover that I haven't and I'll fill in the details.

The Custom Page Layouts

If you open up SharePoint Designer (I know, painful), you'll find all the OOTB layouts in the MasterPage Gallery.  A closer look at many of these reveals that none of them are exactly simple.  So, here's (what I think to be) the foundation mark-up for a blank page layout:

   1: <%@ Page language="C#"   Inherits="Microsoft.SharePoint.Publishing.PublishingLayoutPage,Microsoft.SharePoint.Publishing,Version=12.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" %>
   2: <asp:Content ID="Content2" ContentPlaceholderID="PlaceHolderAdditionalPageHead" runat="server"></asp:Content>
   3: <asp:Content ID="Content3" ContentPlaceholderID="PlaceHolderPageTitle" runat="server"></asp:Content>
   4: <asp:Content ID="Content4" ContentPlaceholderID="PlaceHolderPageTitleInTitleArea" runat="server"></asp:Content>
   5: <asp:Content ID="Content5" ContentPlaceHolderId="PlaceHolderTitleBreadcrumb" runat="server"></asp:Content>
   6: <asp:Content ID="Content6" ContentPlaceholderID="PlaceHolderMain" runat="server"></asp:Content>

To take it a step further, here's one of our 'production ready' custom page layouts.  You'll see that it registers some additional SharePoint assemblies, registers some additional CSS, and puts a breadcrumb control on the page.  Also notice that we register our custom user control and place it in the main content area.



   1: <%@ Page language="C#"   Inherits="Microsoft.SharePoint.Publishing.PublishingLayoutPage,Microsoft.SharePoint.Publishing,Version=12.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" %>
   2: <%@ Register Tagprefix="SharePointWebControls" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
   3: <%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 
   4: <%@ Register Tagprefix="PublishingWebControls" Namespace="Microsoft.SharePoint.Publishing.WebControls" Assembly="Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 
   5: <%@ Register Tagprefix="PublishingNavigation" Namespace="Microsoft.SharePoint.Publishing.Navigation" Assembly="Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
   6: <%@ Register Src="~/_controltemplates/AisHelloWorldControl.ascx" TagName="AisHelloWorldControl" TagPrefix="uc1" %> 
   7: <asp:Content ID="Content2" ContentPlaceholderID="PlaceHolderAdditionalPageHead" runat="server">
   8:     <SharePointWebControls:CssRegistration ID="CssRegistration1" name="<% $SPUrl:~sitecollection/Style Library/~language/Core Styles/rca.css %>" runat="server"/>
   9: </asp:Content>
  10: <asp:Content ID="Content3" ContentPlaceholderID="PlaceHolderPageTitle" runat="server">
  11:      Travel List
  12: </asp:Content>
  13: <asp:Content ID="Content4" ContentPlaceholderID="PlaceHolderPageTitleInTitleArea" runat="server"></asp:Content>
  14: <asp:Content ID="Content5" ContentPlaceHolderId="PlaceHolderTitleBreadcrumb" runat="server">
  15:     <div class="breadcrumb">
  16:         <asp:SiteMapPath ID="siteMapPath" Runat="server" SiteMapProvider="CurrentNavSiteMapProviderNoEncode"
  17:             RenderCurrentNodeAsLink="false" SkipLinkText="" CurrentNodeStyle-CssClass="breadcrumbCurrent" NodeStyle-CssClass="ms-sitemapdirectional"/>
  18:     </div>
  19: </asp:Content>
  20: <asp:Content ID="Content6" ContentPlaceholderID="PlaceHolderMain" runat="server">
  21:         <uc1:AisHelloWorldControl ID="ucMain" runat="server" />
  22: </asp:Content>

The Feature


The feature consisted of two components:  the file modules used to deploy the layouts to the target site, and the feature activation code which we used to provision the pages (more on that in a bit). Chris O'Brien has a great post as part of his series on deploying SharePoint artifacts as features that really helped to guide the way on this and explain some of the peculiarities.  Here's a snippet from our ElementManifest showing how to package a single page layout:



   1: <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   2:     <Module Name="AisCustomPageLayouts" Url="_catalogs/masterpage" Path="." RootWebOnly="TRUE">
   3:         <File Url="AisHome.aspx" Path="AisHome.aspx" Type="GhostableInLibrary">
   4:             <Property Name="Title" Value="Travel System Default Page" />
   5:             <Property Name="ContentType" Value="$Resources:cmscore,contenttype_pagelayout_name;" />
   6:             <Property Name="PublishingPreviewImage" Value="~~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/WelcomeSplash.png, ~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/WelcomeSplash.png" />
   7:             <Property Name="PublishingAssociatedContentType" Value=";#Page;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF39;#"/>
   8:         </File>
   9:     </Module>
  10: </Elements>


The idea here was to create a one shot deployment, so we aimed at eliminating as many manual steps as possible via some FeatureReceiver code.  The following snippet shows how we created the system's own subsite  and  pages.  You'll also notice that the code strips security permissions from the custom page layouts.  This was done to prevent the custom page layouts from showing up as options on the 'Create Page' page.  Wouldn't want anyone hacking our system by creating duplicate pages outside of the locked-down site...  The snippet is kinda long and unorganized (like this post), so let me know if you need any clarification on what I've got here.



   1: public override void FeatureActivated(SPFeatureReceiverProperties properties)
   2: {
   3:     try
   4:     {
   5:         using (SPWeb web = ((SPSite)properties.Feature.Parent).OpenWeb())
   6:         {
   7:             //create the travel system subsite, its groups, and pages.
   8:             CreateSiteAndPages(web);
   9:  
  10:             //strip perm's on layouts so that regular content authors/editors cannot create instances of our custom page layouts
  11:             //(if permission has been removed the page won't show up in the CreatePage list of layouts.
  12:             StripLayoutPermissions(web);
  13:         }
  14:     }
  15:     catch (Exception e)
  16:     {
  17:         System.Diagnostics.EventLog.WriteEntry(this.ToString(), "An error occurred during the FeatureActivated event." +
  18:             Environment.NewLine + e.ToString(), System.Diagnostics.EventLogEntryType.Error);
  19:     }
  20: }
  21:  
  22: private void CreateSiteAndPages(SPWeb web)
  23: {
  24:     //create the travel system site collection.
  25:     SPWeb aisSystemWeb = web.Webs.Add(@SiteUrl,SiteName, "AIS System Site Architecture Prototype", Convert.ToUInt16(1033), web.WebTemplate, true, false);
  26:  
  27:     //create site groups (if they don't already exist) and assign proper permissions.
  28:     CreateAllSiteGroups(web, aisSystemWeb );
  29:  
  30:     PublishingWeb aisSystemPublishingWeb = PublishingWeb.GetPublishingWeb(aisSystemWeb );
  31:     //create the root level pages. 
  32:     CreateAndPublishPage(aisSystemPublishingWeb , "AisHome.aspx", "AIS System Home", true);    
  33: }
  34:  
  35: //create a single publishing page for the specified page layout.
  36: private PublishingPage CreateAndPublishPage(PublishingWeb publishingWeb, string layoutName, string pageName, string title, bool inludeInNav)
  37: {
  38:     //get a list containing all my custom page layouts
  39:     Dictionary<string, PageLayout> layouts  = GetPageLayouts(publishingWeb);
  40:  
  41:     if (!layouts.ContainsKey(layoutName.ToUpper()))
  42:     {
  43:         System.Diagnostics.EventLog.WriteEntry(this.ToString(), String.Format("Unable to create page because the layout '{0}' was not found.", layoutName), 
  44:             System.Diagnostics.EventLogEntryType.Error);
  45:         return null;
  46:     }
  47:  
  48:     PageLayout layout = layouts[layoutName.ToUpper()];
  49:  
  50:     if (pageName == null)
  51:         pageName = layoutName;
  52:  
  53:     PublishingPage page = publishingWeb.GetPublishingPages().Add(pageName, layout);
  54:     page.Title = title;
  55:     page.IncludeInCurrentNavigation = inludeInNav;
  56:     page.IncludeInGlobalNavigation = false;
  57:     page.Update();
  58:     page.CheckIn("");
  59:     page.ListItem.File.Publish("");
  60:     try
  61:     {
  62:         page.ListItem.File.Approve("");
  63:     }
  64:     catch (SPException) { }//not all sites require content approval, but I don't know how to check for this :-)
  65:     return page;
  66: }
  67:  
  68: private  Dictionary<string, PageLayout> GetPageLayouts(PublishingWeb web)
  69: {
  70:     //get a (hardcoded) list of all my custom page layout names.
  71:     Dictionary<string, string> allLayoutNames = GetPageLayoutNameLookupList();
  72:     Dictionary<string, PageLayout> layouts = new Dictionary<string, PageLayout>();
  73:     //this is the content type id for the 'Page' content type
  74:     const string PageLayoutContentId = "0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF39";
  75:     SPContentTypeId pageContentTypeId = new SPContentTypeId(PageLayoutContentId);
  76:  
  77:     foreach (PageLayout layout in web.GetAvailablePageLayouts(pageContentTypeId))
  78:     {
  79:         if( allLayoutNames.ContainsKey(layout.Name.ToUpper()))
  80:             layouts.Add(layout.Name.ToUpper(), layout);
  81:     }
  82:  
  83:     return layouts;
  84: }
  85:  
  86: private void StripLayoutPermissions(SPWeb web)
  87: {
  88:     Dictionary<string, string> allLayoutNames = GetPageLayoutNameLookupList();
  89:     PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(web);
  90:     SPContentTypeId pageContentTypeId = new SPContentTypeId(PageLayoutContentId);
  91:  
  92:     foreach (PageLayout layout in pubWeb.GetAvailablePageLayouts(pageContentTypeId))
  93:     {
  94:         if (allLayoutNames.ContainsKey(layout.Name.ToUpper()))
  95:         {
  96:             SPListItem item = layout.ListItem;
  97:             item.BreakRoleInheritance(false);
  98:         }
  99:     }
 100: }

Using the Ajax Control Toolkit


I sure as.... uhm.... wouldn't be the first guy to blog about the challenges and hoops you must jump through to get the toolkit working in SharePoint, so I won't blather on about it.  I will say, however, that there are definitely some useful posts out there - this post by Mike Ammerlaan being the one that we found most detailed and useful. 


The biggest gotcha we faced was using the <asp:ScriptManager>.  Each of our custom page layouts consisted of one custom User Control.  That User Control registered the required single instance of the ScriptManager.  This worked just fine until we came to integration testing, at which point we realized the portal master page was also the Ajax Control Toolkit, and was registering another ScriptManager on the page.  It took us awhile (and a bunch of 'Unknown Error' messages) to chase this down, but in the end we decided to remove the ScriptManager from the user controls and count on the master page always registering the required single instance.


There are some obvious problems with this approach.  Ideally, I would have preferred to make the user controls smart... have them figure out if the ScriptManager already existed in the page, and if not, do so.  I had no luck with coding something like this, so please chime in here if anyone out there has.  The real problem here is that using the custom page layouts outside of our portal (which uses our custom master page) will always require some type of additional customization/manual configuration.  Yuck.


 


Well... that turned out being much longer than I had originally anticipated, and I feel like there are things I didn't cover that I should have.  Feedback please!