Prexens team dev' corner

How to create custom categories for SharePoint diagnostic logging with IDiagnosticsManager and IDiagnosticsLevel

Download source code and sample (844 KB)
 
Goal of this post
 
I've been working on the past few days on a way to improve the logging infrastructure of our data visualization tool Charting for SharePoint.
 
I thought adding a new category to SharePoint Central Admin diagnostic logging screen would be straightforward. Damned! I'm still wondering why this part of the framework is so skeletal. I found so few documentation on IDiagnosticsManager and IDiagnosticsLevel - and the way to implement this stuff correctly - that I've decided to create a proxy class to make this finally easy.
 
Only very few lines of code are required to achieve that:
 
 
Play with the sample attached
 
The file attached to this post contains a sample implentation of my base class (see Samples folder from the Visual Studio solution)
 
You can try it by deploying Prexens.SharePoint.Diagnostics.wsp (see bin\Solution folder from the Visual Studio solution). This solution basically registers the DLL, provisions a farm level feature containing a receiver which registers the diagnostics manager when activated. The diagnostics manager is deleted when the feature is deactivated.
 
Implement your own diagnostics manager
 
Here are the steps to implement your own diagnostics manager based on my abstract class.
  1. Create a class library assembly and sign it. Add a new class and make it inherit from Prexens.SharePoint.Diagnostics.Samples.BaseDiagnosticsManager (see files attached to this post to get this class).

  2. Implement a parameterless constructor which passes a literal identifier for your diagnostics manager as a parameter of the base class contructor. Call RegisterCategory for any new entry you want to make visible from the list of SharePoint logging categories.   
    public SandboxDiagnosticsManager()
        : base("My diagnostics manager")
    {
        RegisterCategory("My first custom logging category");
        RegisterCategory("My second custom logging category");
    }
  3. Override the method GetCurrentOnFarm(SPFarm farm) and paste the code below. Simply replace SandboxDiagnosticsManager with the name of your own class.
    protected override BaseDiagnosticsManager GetCurrentOnFarm(SPFarm farm)
    {
        return farm.Services.GetValue<SandboxDiagnosticsManager>("My diagnostics manager");
    }
That's it, you diagnostics manager is ready to go. Event and trace severity levels will be automatically persisted to SharePoint config DB when farm admins tweak the event throttling of your custom categories. The SharePoint OO will give you a hook to a cached copy of these settings anywhere, anytime.
 
Next step is to register your diagnostics manager as a SharePoint service.
 
Register your custom diagnostics manager
 
Run the following code in a console app. Again, replace SandboxDiagnosticsManager with the name of your own class
SandboxDiagnosticsManager manager = new SandboxDiagnosticsManager();
SPFarm.Local.Services.Add(manager);
A cleaner way would be to create a farm feature registering your diagnostics manager on activation, and deleting it on deactivation (see attached file for a running sample of this approach).
 
Use your custom diagnostics manager
 
Event and trace severity levels associated with your custom categories can be retrevied this way:
SandboxDiagnosticsManager manager = 
SPFarm.Local.Services.GetValue<SandboxDiagnosticsManager>("My diagnostics manager");

IDiagnosticsLevel customLoggingCategory = manager.GetItem("My first custom logging category");

EventSeverity eventSeverity = customLoggingCategory.EventSeverity;
TraceSeverity traceSeverity = customLoggingCategory.TraceSeverity;
In my case, my custom categories are used in relevant part of the code of Charting for SharePoint to determine if a trace has to be outputted to ULS files according to trace and event severity levels configured through central admin.
 
Updates from my last post on March 6
I've updated my source code to make it Web farm compliant. Update method is indeed different when running code on several WFE.

How-to: Fix "Method 'post' of object 'IOWSPostData' failed" error

Goal of this post

This post aims at providing a step by step solution to an uncommon error which could happen when you try to import an Excel file to a SharePoint 2007 custom list.

Problematic overview

The "Import to a SharePoint List" feature is usually used to create and fill a custom list from an Excel source file.

For an unknown reason, this feature may not work properly on some client workstations whatever the version of SharePoint and Office Excel used. During the import step, the site designer receives the following error message box:


Solution

First we have to point out that this error isn't caused by SharePoint but by Ms Excel, and that the fix will have to be executed on each client workstation - at least site designers workstations - because standard users are not impacted by this error.

The issue will be fixed in two steps: first we need to edit the EXPTOOWS.XLA excel file, and then we have to restore the SharePoint sites collection(s).

Modify EXPTOOWS.XLA
  • Open the Excel add-in "EXPTOOWS.XLA" located by default in "c:\Program Files\Microsoft Office\Office12\<LCID>", where <LCID> is your language identifier (for example, 1033 for a standard English installation, 1036 for a French one).
  • Press ALT+F11 to open the Visual Basic source code editor
  • Comment out the following line : "lVer = Application.SharePointVersion(URL)" by adding a simple quote at the beginning of the line
  • Below the commented line, add the following instruction : IVer = 2
  • Save your changes, and restart the client machine.
Backup and restore site the sites collection

The Excel install is now fixed, and you will be able to import Excel files in custom SharePoint list on the new SharePoint site collections. But in order to make this solution work on the actual site collection, you will have to backup and restore it by using STSADM command line tool which is provided with SharePoint (more information on STSADM command line tool are available by following this link : http://technet.microsoft.com/en-us/library/cc261956.aspx)


Execute the following commands :

stsadm -o backup -url http://PORTAL-URL -filename c:\Backups\BACKUP-FILE.dat

stsadm -o restore -url http://PORTAL-URL -filename c:\Backups\BACKUP-FILE.dat -overwrite


You can now start again the custom list importation from an Excel file, it should now work properly.


How to debug custom STSADM commands

When developing custom STSADM commands, it could be handy to debug our code in the real execution context of STSADM. To do so, you only have to deal with your Visual Studio project configuration.
 
1. Right click your VS project and go to properties
 
2. From Build Events tab, paste the following commands into the Post-buid event command line box
 
"%programfiles%\Microsoft Visual Studio 8\SDK\v2.0\Bin\gacutil.exe" /uf $(TargetName)
"%programfiles%\Microsoft Visual Studio 8\SDK\v2.0\Bin\gacutil.exe" /if "$(TargetPath)"
 
3. From Debug tab, choose Start external program as a Start Action and browse for file C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\BIN\STSADM.EXE
 
 
4. In the Command line arguments text box, specify -o <name of your command> and the rest of your input parameters
 
 
5. You can now add break points to your code, run your project and debug your code in real conditions

Ultimate solution to get rid of "The Web site wants to run the following add-on: 'Name ActiveX Control'"

Download setup files (21.1 KB)
Download sources (926 KB)
 
Problematic overview
 
When you are developing internet or extranet website with SharePoint, a common issue you have to resolve is to fix the following message displayed to end-users running IE7 and later
 
The Web site wants to run the following add-on: 'Name ActiveX Control'
 
This message is caused by presence features of SharePoint requiring this ActiveX control to be installed on clients computer. Many solutions can be found on the Web. Even Microsoft gives some workarounds to get rid of this behavior. But to me, none of them are really reliable.
 
Let's study a bit solutions from Microsoft. First and second methods require doing something on clients computer... Hmmm, seems to be a bit tricky in case of an internet or extranet Web site!
 
Solution #3 is to duplicate and rename the JS file which contains the presence script, comment out some code and register this new version by specifying its name through the name attribute of a ScriptLink object. Great, it does the trick! But what if I want to be a good guy and do not load core.js for my branding website? Let's remind the solution for this tweak: I need to empty script link of any name attribute... a dog eating his tail. And what if I want to have presence features enabled when my users are Windows authenticated?
 
A true Best-of-Breed solution: Control Adapters
 
Control Adapter is a very handy solution when you want to update the renderings of WebControls you are not responsible for. In our case, the trick is to create a control adapter for Microsoft.SharePoint.WebControls.ScriptLink and change the rendering behavior when authentication method is not Windows based. If the user is anonymous or form-based authenticated, we load a light version of the presence JS script and we suppress core.js.
 
Let's talk about code
 
First, we create a class library project in Visual Studio, sign the assembly and create a class ScriptLinkAdapter inheriting from System.Web.UI.Adapters.ControlAdapter.
 
Then, we override the Render method and we paste the code below.
 
   1:  // Retrieve authentication type
   2:  string authenticationType = HttpContext.Current.User.Identity.AuthenticationType;
   3:   
   4:  // Replace output with a minimal output if authentication is anonymous or forms-based
   5:  if (!HttpContext.Current.Request.IsAuthenticated ||
   6:      String.IsNullOrEmpty(authenticationType) ||
   7:      authenticationType.Equals("Forms", StringComparison.InvariantCultureIgnoreCase))
   8:  {
   9:   
  10:      // Load the ouputted HTML in memory
  11:      using (MemoryStream memoryStream = new MemoryStream())
  12:      using (StreamWriter streamWriter = new StreamWriter(memoryStream))
  13:      {
  14:          using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(streamWriter))
  15:          {
  16:              Control.GetType().InvokeMember(
  17:                  "RenderControl",
  18:                  BindingFlags.InvokeMethod 
  19:                  | BindingFlags.Instance 
  20:                  | BindingFlags.NonPublic,
  21:                  null,
  22:                  Control,
  23:                  new object[] { htmlTextWriter, null });
  24:          }
  25:   
  26:          // Flush the writer and rewind the stream
  27:          streamWriter.Flush();
  28:          memoryStream.Position = 0;
  29:   
  30:          // Read the stream as a string
  31:          using (StreamReader streamReader = new StreamReader(memoryStream))
  32:          {
  33:              string output = streamReader.ReadToEnd();
  34:   
  35:              // Remove core.js
  36:              string corePattern = @"\<script.+src\=.+core\.js.+\<\/script\>";
  37:   
  38:              output = Regex.Replace(output, corePattern, "", RegexOptions.IgnoreCase);
  39:   
  40:              // Replace init.js by a version excluding presence script
  41:              string initPattern = @"(\<script.+src\=\"")(init\.js)(\"".+\<\/script\>)";
  42:   
  43:              if (Regex.IsMatch(output, initPattern))
  44:              {
  45:                  output = Regex.Replace(output,
  46:                      initPattern,
  47:                      String.Format("$1{0}$3", Page.ClientScript.GetWebResourceUrl(
  48:                              typeof(ScriptLinkAdapter),
  49:                              "ScriptLinkAdapter.ClientResources.SPInitNoPresence.js")),
  50:                      RegexOptions.IgnoreCase);
  51:              }
  52:   
  53:              // Flush new content to response
  54:              writer.Write(output);
  55:          }
  56:      }
  57:   
  58:      // Registrations are done, so exit.
  59:      return;
  60:  }
  61:   
  62:  // Standard control rendering
  63:  Control.GetType().InvokeMember(
  64:      "RenderControl",
  65:      BindingFlags.InvokeMethod 
  66:      | BindingFlags.Instance 
  67:      | BindingFlags.NonPublic,
  68:      null,
  69:      Control,
  70:      new object[] { writer, null });
 
First, we retrieve the authentication type to apply transformation only for anonymous and forms authenticated users (line 1 to 8)
 
A key thing to know about control adapters is how to render the adapted control in standard way if no transformation has to be applied. A common mistake is to make a call to the RenderControl of the adapted control. If you do so you will enter an infinite loop: all controls are designed to look for an adapter before rendering themselves, if an adapter is found then the adapter is used and if the adapter call then render method again, the control will look for an adapter which calls the render... the dog and his tail again!

The solution is to use reflection to call a protected version of RenderControl that take a ControlAdapter as an input parameter. If we pass null, no adapter is assumed and the normal rendering cycle is called (line 14 to 24).
 
We are now able to get the original HTML content produced when rendering a ScriptLink control normally. We load this content into a MemoryStream, make some changes on it to suppress core.js and replace the old version of init.js and we flush this new content to the HtmlTextWriter attached to the Response object (line 33 to 54).
 
How to deploy
  1. Register assembly Prexens.SharePoint.WebControls.Adapters.ScriptLink.dll to the GAC of your SharePoint server
  2. Copy file Prexens.SharePoint.WebControls.Adapters.ScriptLink.browser to the folder App_Browsers of the application folder of your Web application (typically C:\Inetpub\wwwroot\wss\VirtualDirectories\<Your App Pool>)
  3. IMPORTANT: open the file App_Browsers\compat.browser in Notepad and save it (no need to change anything). This will force reloading the browser files. If you have multiple browser files already present you might need to repeat for each one.
  4. Restart IIS services (iisreset.exe in a command prompt)

Categories
Links
  Prexens Web Site
  Charting for SharePoint
  Charting for SharePoint (Codeplex)
  Charting for SharePoint Starter kit

Microsoft Certified Partner