Category: Development

StaticName != InternalName

Recently I was trying to fetch a SPField from a SPWeb object. I had SharePoint 2010, so I decided to use the new SPFieldCollection.TryGetFieldByStaticName() Method.

image

You can imagine how surprised I was, that I couldn’t get the field I was looking for. What do we learn? Well, the StaticName of an SPField is not necessarily the InternalName!

Here is a link to the MSDN about SPField.StaticName: http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spfield.staticname.aspx

Watch out for ContentTypeBindings

If you don’t know ContentTypeBindings, take a short look at: http://msdn.microsoft.com/en-us/library/aa543598.aspx

“Content type binding enables you to provision a content type on a list defined in the onet.xml schema.”

So we can assign content types to newly created lists. That’s cool 🙂  The ContentTypeBinding feature can, of coarse, contain multiple content types which are bound to multiple lists. Like this:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   <ContentTypeBinding 
      ContentTypeId="0x0100yourGuid" 
      ListUrl="Pages" />
   <ContentTypeBinding 
      ContentTypeId="0x0100anotherGuid" 
      ListUrl="Pages" />
   <ContentTypeBinding 
      ContentTypeId="0x0100yetAnotherGuid" 
      ListUrl="Lists/YourList" />
</Elements>

There is however, a limitation! Do not configure more then one ContentTypeBinding feature for a newly created page! You will get a save conflict Exception, when you provision a new web.

If you are curious how the feature gets referenced, take a look at this page: Understanding Onet.xml files

<Configurations>
   ...
   <Configuration ID="0" Name="Default">
      <Lists>
         ...
      </Lists>
      <Modules>
         <Module Name="Default" />
      </Modules>
      <SiteFeatures>
         ...
      </SiteFeatures>
      <WebFeatures>
         <!-- a ContentTypeBinding feature -->
         <Feature ID="6BB8BC13-987F-4668-9A63-E42F1CC03C44" />
         <!-- do NOT add another ContentTypeBinding feature! -->
         <Feature ID="CFAF8323-0CC5-4426-A33D-5B6A0AD72F96" />
      </WebFeatures>
   </Configuration>
   ...
</Configurations>

Empty Admin Recycle Bin items

What is it?

Usually the size of the recycle bin is not relevant. But on development machines, you don’t want lots of files in there, which make your databases grow without actually used data.

What do you do? Go to the recycle bin, click on “Site Collection Recycle Bin”. The two stages of the recycle bin can be managed independently.

image

The two views on the left let you switch between the first- and second stage. All items from the first stage can be moved to the second stage with one click (+ 1 confirmation). But if the items are in the second stage, you can only delete 200 items at a time by selecting all and delete them in batches. An option to delete all items is missing (in the GUI).

image

My solutions adds this option for the second stage recycle bin. A link for deleting all items at once is displayed in the toolbar. If you click it, a modal dialog – I love SharePoint 2010 🙂 – is displayed, and you can delete all items at once.

image

 

How does it work?

A feature (farm-scoped) activates a delegate control. It references jQuery/JavaScript code to add the button “Empty Recycle Bin”, as shown in the second picture, and to open a modal dialog.

This modal dialog is a custom application page, which will query the recycle bin for statistics (as shown in the second picture) and delete items in batches via SPRecycleBinQuery which is similar to the SPQuery.

New link in the toolbar

jQuery was my choice to add an additional button the the toolbar of the second stage recycle bin. The developer tools from the Internet Explorer helped me to find the right table by its ID. The content of it is modified, so the button is injected via JavaScript. That way there is not server code required to add the button directly to the page.

jQuery(document).ready(function () {
   // the Toolbar2 is used for the second stage recycle bin
   var row = jQuery("#ctl00_PlaceHolderMain_Toolbar2 > tbody > tr");
   
   // find last cell
   var lastTD = $(row).find('td:last');

   // add seperator
   lastTD.before('<TD class=ms-separator>|</TD>');

   // change content of the last cell
   var content =
   '<TABLE border=0 cellSpacing=0 cellPadding=1>' +
      '<TBODY>' +
         '<TR>' +
            '<TD class=ms-toolbar noWrap><A class=ms-toolbar title=' + emptyRecycleBin + ' href="javascript:openEmptyAdminRecycleBinDialog()">...</A></TD>' +
            '<TD class=ms-toolbar noWrap><A class=ms-toolbar title=' + emptyRecycleBin + ' href="javascript:openEmptyAdminRecycleBinDialog()">' + emptyRecycleBin + '</A>...</TD>' +
         '</TR>' +
      '</TBODY>' +
   '</TABLE>';
   lastTD.html(content);
});

Modal Dialog

The modal dialog is opened with JavaScript:

function openEmptyAdminRecycleBinDialog() {
   var options = {
      url: "../../_layouts/RH.EmptyAdminRecycleBin/EmptyAdminRecycleBin.aspx",
      width: 400,
      height: 130,
      title: "Empty Admin RecycleBin",
      dialogReturnValueCallback: onDialogClose
   };
   SP.UI.ModalDialog.showModalDialog(options);
}

function onDialogClose(dialogResult, returnValue) {
   if (dialogResult == SP.UI.DialogResult.OK) {
      // refresh the page to reflect changes
      SP.UI.ModalDialog.RefreshPage(SP.UI.DialogResult.OK)
   }
   if (dialogResult == SP.UI.DialogResult.cancel && returnValue != null) {
      alert(returnValue);
   }
}

With the callback function, a page refresh is triggered. Otherwise the deleted items would still be visible on the recycle bin page.

Recycle Bin

Querying recycle bin items is achieved with a dedicated object. The SPRecycleBinQuery. It takes parameters like the RowLimit. If you don’t specify the RowLimit, only 50 items will be returned. The ItemState defines if items from the first, or second stage will be returned. The query is executed multiple times, to get all items.

private void GetRecycleBinStorageInfo(out int itemCount, out long overalSize)
{
   itemCount = 0;
   overalSize = 0;
   SPRecycleBinItemCollectionPosition itemcollectionPosition = null;
   do
   {
      SPRecycleBinQuery query = CreateQuery(itemcollectionPosition);
      SPRecycleBinItemCollection recycleBinItems = Site.GetRecycleBinItems(query);
      itemcollectionPosition = recycleBinItems.ItemCollectionPosition;
      // get itemCount
      itemCount += recycleBinItems.Count;
      // get overalSize
      overalSize += recycleBinItems.Cast<SPRecycleBinItem>().Sum(item2 => item2.Size);
   } while (itemcollectionPosition != null);
}

private static SPRecycleBinQuery CreateQuery(SPRecycleBinItemCollectionPosition page)
{
   var query = new SPRecycleBinQuery
                  {
                     RowLimit = 200,
                     ItemState = SPRecycleBinItemState.SecondStageRecycleBin,
                     OrderBy = SPRecycleBinOrderBy.Default,
                     ItemCollectionPosition = page ?? SPRecycleBinItemCollectionPosition.FirstPage
                  };
   return query;
}

The method returns the itemcount and size of all items in the second stage recycle bin.

To delete all items, I’ve used this code:

using (var operation = new SPLongOperation(this))
{
   operation.Begin();

   try
   {
      // delete all items in pages of 200 items
      SPRecycleBinItemCollectionPosition itemcollectionPosition = null;
      do
      {
         SPRecycleBinQuery query = CreateQuery(itemcollectionPosition);
         SPRecycleBinItemCollection recycleBinItems = Site.GetRecycleBinItems(query);
         itemcollectionPosition = recycleBinItems.ItemCollectionPosition;
         for (int i = 0; i < recycleBinItems.Count; i++)
         {
            recycleBinItems[i].Delete();
         }
      } while (itemcollectionPosition != null);

      operation.End("/_layouts/RH.EmptyAdminRecycleBin/CloseModalDialog.html", SPRedirectFlags.Default, Context, null);
   }
   catch (Exception ex)
   {
      UlsLogging.Write(TraceSeverity.Unexpected, ex.ToString());
      SPUtility.TransferToErrorPage(ex.Message);
      return false;
   }
   return true;
}

The SPLongOperation shows the nice animation during processing the code. Just make sure you end the operation to hide the animation.

Exception Handling

Usually my classes have a method HandleException, as the WSPBuilder does :-). This class will write the exception to the SharePoint ULS log.

private void HandleException(Exception ex)
{
   try
   {
      UlsLogging.Write(TraceSeverity.Unexpected, ex.ToString());
      Controls.AddAt(Controls.Count, new Label {CssClass = "ms-error", Text = ex.Message});
   }
   catch (Exception e)
   {
      Trace.Write(e.ToString());
   }
}

internal class UlsLogging
{
   internal static void Write(TraceSeverity traceSeverity, string message)
   {
      var uls = SPDiagnosticsService.Local;
      if (uls != null)
      {
         SPDiagnosticsCategory cat = uls.Areas["SharePoint Foundation"].Categories["Web Controls"];
         uls.WriteTrace(1, cat, traceSeverity,message, uls.TypeName);
      }
   }
}

If you want to use an area or category which is not a default one, you’ll need administration permissions to create it. The use of RunWithElevatedPrivilegues is not enough!

You can download the sourcecode here: RH.EmptyAdminRecycleBin(Sourcecode).zip

The compiled solution as WSP file can be downloaded here: RH.EmptyAdminRecycleBin.wsp

And before you ask: The solution is for SP 2010! SharePoint Services 3 lacks the modal dialog. But you can take the source code, and modify it to fulfill your needs.

PortalSiteMapProvider.GetCachedListItemsByQuery

With MOSS 2007 or SharePoint Server 2010 you can use the PoraltSiteMapProvider of the Microsoft.SharePoint.Publishing.dll assembly to retrieve cached listitems.

   1: PortalSiteMapProvider ps = PortalSiteMapProvider.WebSiteMapProvider;
   2: var pNode = ps.FindSiteMapNode(web.ServerRelativeUrl) as PortalWebSiteMapNode;
   3: var query = new SPQuery
   4:                {
   5:                   Query = "<Where><Neq><FieldRef Name='ID' /><Value Type='Counter'>0</Value></Neq></Where>"
   6:                };
   7: SiteMapNodeCollection quoteItems = ps.GetCachedListItemsByQuery(pNode, "Top Seiten", query, web);

In my case, I didn’t need any special where clause. I wanted to retrieve all items, so I left the Query property  empty. And because I needed only three columns, I specified the ViewFields property of the SPQuery object.

Bad idea. The query failed hard and fast 🙂

Conclusion:

If you use the GetCachedListItemsByQuery method, do not specify the ViewFields property of the SPQuery and configure a query. Even if it returns all items of the list!

CKS:EBE 3.0-Enhanced Blog Edition 3.0

Like many other blogs running SharePoint, my blog uses the EBE to add more functionality to the default SharePoint blog.

And since I am one of the developers of the EBE 3.0, I’m glad that we announce the release of the next release. Version 3 brings along many new features and improvements of already implemented features.

New Features
*Ability to theme wiki pages
*Ability to export post to PDF
*Localization (French, Spanish)
*Technorati Links from post categories
*Ability to bookmark post with Twitter
*Centralized Theming – Ability to create a theme library at the root and allow sub blog sites to use the common theme library.
*The ability to add an XML feed control
*Logging of pingbacks and trackback errors to SharePoint Logs directory
*Support of feature stapling
*Preliminary SharePoint 2010 Beta 3 compatible (with web.config edits)
*EBE caching and performance validation
*Performance increases for page loads less than <3 sec
Note: Some features are specific to certain themes

Enhancements
*Caching enhancements
*Added caching to XML controls
*Added enhanced XSL caching
*Ability to exclude the EBE HttpModule from specific paths
*Auto-Discovery for Live Writer metaweblog api
*Tweaks and enhancements to all themes
*Ability to sign-in after denied access to system pages
*Posts with future date are now hidden from posts list
*Browser title now matches post titles
*Comments are not added if they are spam

Read the release notes here: http://cks.codeplex.com/releases/view/28520

SharePoint 2010 SDK

The Microsoft SharePoint 2010 Software Development Kit (SDK) contains conceptual overviews, programming tasks, samples, and references to guide you in developing solutions based on SharePoint 2010 products and technologies.

You can grab it here: http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=f0c9daf3-4c54-45ed-9bde-7b4d83a8f26f

The download contains the SDK for SPF and SPS. No need to download two separate files anymore.

The SharePoint Developer Center can be found here: http://msdn.microsoft.com/en-us/sharepoint/default.aspx

The IWebPartField Interface versus the ASP.NET Lifecycle

A Webpart receives a filter value through the IWebPartField interface. The example over at MSDN was simple and clean. So I adopted the code to my Webpart.

A common scenario would be to create controls based on the received filter value. E.g. query a list for the passed filter value, and display the item from the query.ASP.NET Lifecycle

Problem

From the ASP.NET Lifecycle we know how to deal with controls. Create them in CreateChildControls, assign values in OnPreRender and let them being rendered in RenderContents. Now we have a problem. The passed value in our Webpart is not available at any place, where we can still add controls to the page. If we try to do in RenderContens, they are not shown. It’s too late.

The example stores the passed value in

private object _fieldValue;

On OnPreRender a method is called, which will process the filter data.

protected override void OnPreRender(EventArgs e)
{
   if (_provider != null)
   {
      _provider.GetFieldValue(new FieldCallback(GetFieldValue));
   }
   base.OnPreRender(e);
}

The example will render the received value in RenderContents

protected override void RenderContents(HtmlTextWriter writer)
{
   if (_provider != null)
   {
      PropertyDescriptor prop = _provider.Schema;

      if (prop != null && _fieldValue != null)
      {
         writer.Write(prop.DisplayName + ": " + _fieldValue);
      }...

Unfortunately this is too late for me. As stated above, you can’t add controls to the Controls collection of a page/Webpart anymore.

Solution

As soon as the value is being received from the Webpart connection, we use it to create controls or do stuff with it before the lifecycle reaches Render.

Instead of the private field _fieldValue, I used a property “FieldValue” with a backing field “_FieldValue”. The setter of the property assigns the new value to _FieldValue and triggers another method which will create controls. Assigning the value is just before Render.

private object _FieldValue
public object FieldValue 
{ 
   get { return _FieldValue; } 
   set
   { 
      _FieldValue = value; 
      DoStuff();
   }
}

Modify Overwrite Policy for an EventLog created by an EventLogInstaller

Developing a Windows Service is a common task. Creating an EventLog for this service also is a common practice. The EventLog can be created with a ServiceInstaller (a class which inherits from System.Configuration.Install.Installer).

This is how an Eventlog can be added during the installation with a custom Installer class:

public Installer()
   {
      Installers.Add(new EventLogInstaller
               {
                  Source = "EventLogSource",
                  Log = "EventLogName"
               });

The EventLogInstaller class does not allow to set the overwrite mode.

You can change the overwrite mode after the log has been created, if you use the Committed Event.

public Installer()
{
   ...
   Committed += OnCommitted;
}

/// <summary>
/// modify the eventlog
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void OnCommitted(object sender, InstallEventArgs e)
{
   EventLog log = EventLog.GetEventLogs().SingleOrDefault(l => l.Log == "EventLogName");
   if (log == null) throw new InvalidOperationException("The log does not exist");
   log.ModifyOverflowPolicy(OverflowAction.OverwriteAsNeeded, 30);
}

Google Analytics data in a SharePoint list

Jacob Reimers has written an C# API for accessing Google Analytics data.image

When I saw his post, I thought it would be great to fetch the latest data every day, and write it to a SharePoint list. Then you can use the data, without querying Google for every request.

You could then use the ChartPart for SharePoint to generate nice charts from your Google Analytics data.

E.g. one for the visits for the last month. Or from which country your visitors come.image

So how do I get the data?

A console application is started via task scheduler every day. It fetches the data from the previous day, and stores it in my local SharePoint list.

image

The list and all required fields are created, if it does not exist.

There are some parameters, which will be used:

  • WebUrl – specify the Url to a SharePoint site
  • ListName – a list in the site (title of the list)
  • GoogleLoginName – your account name with Google
  • GooglePassword – your Google account password
  • GoogleTitle – the title of a website within Google Analytics
  • StartDate – optional. If not specified, the last day will be used

With the parameters, the call to the application might look like this:

RH.ImportGoogleAnalytics -WebUrl=http://sharepointurl/weburl -ListName="Google Analytics" -GoogleLoginName=your@email.tld -GooglePassword=YourPassword -GoogleTitle=www.yourdomain.tld [-StartDate=mm.dd.yyyy]

If the StartDate parameter is omitted, data for the last day will be fetched.

Of course you can change the code to get different data from Google Analytics. There is plenty of information. You can take a look at the data with the Data Feed Query Explorer.

  Download the program and source code

SPFarm.Local is null?

It took me some time to find the solution to a problem, where a WPF application could not connect to a local SharePoint farm via SPFarm.Local. It always returned null.

Windows 7 was not the problem. I copied my application to a Windows Server 2008 VM to test.

In case somebody has the same problem, here is the solution:

image

Make sure you have set the Platform to “Any CPU” and not x86 if you are using x64 assemblies.

Update:

SPFarm.Local wants to create a connection to the configuration database. In order to do so, you’ll need to have permissions on the configuration database. Usually the service account and the application pool accounts can read the configuration database.
If you don’t have the right to read the configuration database, SPFarm.Local will be null!

The same permissions are required if you want to access SPAdministrationWebApplication.Local

Update2:

There is an entry on connect. You can vote for “Any CPU” as default target:

https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=455333&wa=wsignin1.0

SPDispose or not to SPDispose

Roger Lamb has posted an article about SharePoint objects, which must not be disposed. He also wrote about changes to SP 2010 regarding disposing objects.

  • SPContext.Current.Site
  • SPContext.Current.Web
  • SPContext.Site
  • SPContext.Web
  • SPControl.GetContextWeb(..)
  • SPControl.GetContextSite(..)
  • SPFeatureReceiverProperties.Feature.Parent
  • SPItemEventProperties.ListItem.Web
  • SPList.BreakRoleInheritance()
    • Do not call list.ParentWeb.Dispose()
  • SPListEventProperties.Web
  • SPListEventProperties.List.Web
  • SPSite.RootWeb
    • Problems may occur when SPContext.Web has equality to the SPContext.Web.. make sure you dispose of SPSite and it will cleanup sub webs automatically
  • SPSite.LockIssue
  • SPSite.Owner
  • SPSite.SecondaryContact
  • SPWeb.ParentWeb
  • SPWebEventProperties.Web

More on his blog: http://blogs.msdn.com/rogerla/archive/2009/11/30/sharepoint-2007-2010-do-not-dispose-guidance-spdisposecheck.aspx

Getting started with Silverlight development

Tim Heuer has written 7 posts about Silverlight development. So if you haven’t started with Silverlight but plan to, have a look at his post series.

CodePlex project

Develop your own Windows Service

Writing a Windows Service is very easy. Deploying it with a Setup is an easy task as well.

But be careful what option you set for the failure behavior!

In my case I installed the service with “Windows Installer XML (WiX) toolset”.

image

Do not use critical as ErrorControl value. It will force a reboot of the server, if the services fails to start.

image

Fortunately booting with “Last known good configuration” worked for me and I could uninstall the service…

Get Central Administration Webapplication

With this code you can get the central administration webapplication.

   1:  private static SPWebApplication GetCentralAdministrationWebApplication()
   2:  {
   3:      SPWebService cS = SPWebService.ContentService;
   4:      var service = cS.Farm.Services.GetValue<SPWebService>("WSS_Administration");
   5:      SPWebApplicationCollection webApplications = service.WebApplications;
   6:      foreach (SPWebApplication webApplication in webApplications)
   7:      {
   8:          return webApplication;
   9:      }
  10:      return null;
  11:  }

If you have a better way, let me know 🙂

And here we go. A big Thank you goes to Axel Heer.

return SPAdministrationWebApplication.Local;

Control.ClientID has wrong value

An ASP.NET Controls has a ClientID property. SharePoint Controls inherit from the ASP.NET Controls. The property will give you the ID, the rendered control will have in the HTML source. There is one thing to remember:

The ClientID is valid only, if the control has been added to the Controls of the Page!

ID before adding the controlID after adding the control
FilterButtonctl00_m_Webpart1_FilterButton

So if you need the ClientID e.g. to pass it to a JavaScript to be able to find the control, make sure you grab the ClientID after the control has been added to the Page.

New WSPBuilder version

Carsten Keutmann has released a new version of his great WSPBuilder. Here are the new features:

  • A Reference folder under GAC and 80/Bin is now supported for large dlls that do not need reflection for SafeControls and Permissions.
  • New menu function "Recycle the Windows SharePoint Services Timer" implemented on WSPBuilder Extensions.
  • Refactoring of "Copy to GAC" in order to improve the functionality.
  • Bug fixed! "Unable to get solution id from manifest: Unable to extract manifest.xml …" error for x64 systems.
  • CabLib.dll updated to new version 10.5.
  • Its now possible to include the Cablib.dll into the WSP package.

The download (incl. source code) is available on CodePlex: Download WSPBuilder