Blog Archives

Must have tools for BCS development

Creating a BCS model and external list with SharePoint Designer 2010 makes things a breeze: just few clicks and we are done, but this typically does not work in development environment. Why? Simply because we need to package this up, and to package this up, we need to be able to extract the BCS artifacts into a Visual Studio solution.

Well, lucky that there are tools for that, check out this blog post by Russell Palmer. These tools are:

  1. BCS Artifact generator
  2. BCS Solution Packaging tool
Advertisements

Writing to event log and ULS in Sharepoint 2010

No logging means no visibility

Logging is a prime and most basic feature that any software need to have, and there are many ways to implement logging. In this post, I will illustrate a way to implement logging to windows application event log and/or the ULS when developing custom sharepoint 2010 solutions.

The assumption here is that the solution will be deployed as a farm solution.

A log entry can have various attributes or properties, but most likely we would want to use the following attributes:

  1. Source
  2. Severity
  3. Correlation ID
  4. Message

When writing to the application event log, it is advisable to create the event sources beforehand. Identify your event sources that you want to use and create these sources, one way is to use the script in this post.

Typically I would advise against logging to the application event log on production systems, for sharepoint solutions, use the ULS, but alas, reading ULS every time things goes wrong can be time consuming, yeah we have great tools like the ULS viewer, but it is still not as easy to read as the event viewer. Therefore, I typically write my logs to the application event log on my DEV machine.

For this, I need a switch that enables me to direct all my log entries to the application event log, the ULS, or both (or other sources). Astute .NET developers would probably think of Enterprise Library when it comes to this scenario. Yes EntLib is collection of great components, but if all we want is a simple logger that writes to two sources, we might as well write common logic and be done in 30 minutes or less rather than trying to bake in and configure EntLib, think about the web.config changes you have to do with EntLib.

For me, my switch is simply a couple of constants in a static class:

namespace Department.Solution.Common
{
    public static class Constants
    {
        public static class AppSettings
        {
            public const bool LOG_TRACE = true;
            public const bool LOG_EVENTLOG = true;
        }
    }
}

We should also ideally put our event sources in constants:

namespace Department.Solution.Common
{
    public static class Constants
    {
        public static class LogSource
        {
            public const string LOGSRC_COMMON = "ProductXYZ Common";
            public const string LOGSRC_WEBPART = "ProductXYZ WebParts";
        }
    }
}

Then I have these methods in my common logger class to write to event log and/or the ULS depending on those flags:

private static void WriteToTrace(Guid correlationGuid, string source, string message, EventLogEntryType entry)
{
    if (!Constants.AppSettings.LOG_TRACE) return;
    var diagSvc = SPDiagnosticsService.Local;
    var traceSeverity = TraceSeverity.Unexpected;
    var evtSeverity = EventSeverity.Error;

    switch (entry)
    {
        case EventLogEntryType.Information:
        case EventLogEntryType.FailureAudit:
        case EventLogEntryType.SuccessAudit:
            traceSeverity = TraceSeverity.Verbose;
            evtSeverity = EventSeverity.Information;
            break;
        case EventLogEntryType.Warning:
            traceSeverity = TraceSeverity.Medium;
            evtSeverity = EventSeverity.Warning;
            break;
        default:
            traceSeverity = TraceSeverity.Unexpected;
            evtSeverity = EventSeverity.Error;
            break;
    }
    SPSecurity.RunWithElevatedPrivileges(() =>
        diagSvc.WriteTrace(0,
        new SPDiagnosticsCategory(source, traceSeverity, evtSeverity), traceSeverity,
        "Correlation Guid {0}: {1}", new object[] { correlationGuid.ToString("B"), message })
    );
}

private static void WriteEventLogEntry(Guid correlationGuid, string source, string message, EventLogEntryType entry)
{
    if (!Constants.AppSettings.LOG_EVENTLOG) return;
    SPSecurity.RunWithElevatedPrivileges(() =>
            {
                var appLog = new EventLog { Source = source };
                appLog.WriteEntry(string.Format("Correlation Guid {0}{1}{2}", correlationGuid.ToString("B"), System.Environment.NewLine, message), entry);
            });
}

Simply put wrappers and overrides around these two methods, for example:

public static string LogError(string source, string format, params object[] args)
{
    return LogError(source, string.Format(format, args));
}
public static string LogError(string source, string message)
{
    Guid correlationGuid = Guid.NewGuid();
    WriteEventLogEntry(correlationGuid, source, message, EventLogEntryType.Error);
    WriteToTrace(correlationGuid, source, message, EventLogEntryType.Error);
    return correlationGuid.ToString();
}

Then whenever you want to log anything, simply call the public method and pass in your source string in one line (for example, if the logger is implemented within your custom LoggerInstance class):

LoggerInstance.LogError(Constants.LogSource.LOGSRC_WEBPART, "Woops, web part {0} errored out!", webPart.Title);

Logging would be a breeze if you have this helper class, because it simply provide a layer of abstraction.

Viewing XML data on Content by Query Web Part (CQWP)

How many times have you been tasked to customize a Content by Query Web Part in SharePoint? In my case, this task often involves customizing the XSL used by the web part. The pain is, without knowing the data we are working with, we are basically trying to aim our arrows in darkness.

If we can see the data (ie, XML) that we are dealing with, then it would become much easier to write an XSL that achieves what we want. This post aims to give you a way to dump out the XML content of the content query web part to allow you to visualize what you are working with.

To do this, first step is to download a copy of the default xsl that all CQWP uses, this usually exist in Style Library/XSL Style Sheets subfolder and is called ContentQueryMain.xsl
Download this to your local folder now, and rename it as something else, eg. ContentQueryDump.xsl

Now open the ContentQueryDump.xsl file in your favourite XSL editor or text editor, and locate the template root node

Change the inner XML of this node to dump out the xml content by surrounding it in TEXTAREA tag

<xsl:template match="/">
    <TEXTAREA style="width:200px;" rows="10">
        <xsl:copy-of select="*"/>
    </TEXTAREA>
</xsl:template>

Save this file, and upload it to the Style Library within the XSL Style Sheets folder.

Now picture this: I have a custom list in my site that contains 2 custom columns, and I want a Content by Query Web Part to display data on this list according to the XSL that I specify.

I set up a content by query web part on my home page, set the query scope to my custom list, and this is what I get:

Let us take this web part and dump its XML data on screen. Export the web part from your page, and save it locally:

Crack it open in your favourite XML Editor or text editor, then locate the property node with name attribute of MainXslLink. Put the reference to the xsl file you uploaded earlier, as follows:

<property name="MainXslLink" type="string">
    /Style Library/XSL Style Sheets/ContentQueryDump.xsl
</property>

Save this .webpart file, then import and add this web part to the page

The result is as follows:

And voila! You have your content result XML, this is what your XSL will be looking at. You can see all the attributes, which makes things much easier from here onwards.

<dsQueryResponse>
  <Rows>
    <Row Title="Item 3" FileRef="/Lists/MyList/3_.000" Author="George Lucas" Editor="George Lucas" FSObjType="0" PermMask="0x400000300c231061" ProgId="" File_x005F_x0020_Type="" File_x005F_x0020_Size="" PublishingRollupImage="" Comments="" ID="3" Modified="2011-11-02 11:33:02" Created="2011-11-02 11:33:02" _Level="1" WebId="bf499d62-67d5-46af-9746-acd74885fa45" ListId="19877173-9e7a-4543-962b-c4dc4261aceb" _x007B_94f89715_x002D_e097_x002D_4e8b_x002D_ba79_x002D_ea02aa8b7adb_x007D_="Lists/MyList/3_.000" PubDate="Wed, 02 Nov 2011 00:33:02 GMT" FileExtension="" FileSize="" DocumentIconImageUrl="/_layouts/IMAGES/icgen.gif" LinkUrl="http://localhost/Lists/MyList/3_.000" Style="Default" GroupStyle="DefaultHeader" __begincolumn="True" __begingroup="False"></Row>
    <Row Title="Item 2" FileRef="/Lists/MyList/2_.000" Author="George Lucas" Editor="George Lucas" FSObjType="0" PermMask="0x400000300c231061" ProgId="" File_x005F_x0020_Type="" File_x005F_x0020_Size="" PublishingRollupImage="" Comments="" ID="2" Modified="2011-11-02 11:32:47" Created="2011-11-02 11:32:47" _Level="1" WebId="bf499d62-67d5-46af-9746-acd74885fa45" ListId="19877173-9e7a-4543-962b-c4dc4261aceb" _x007B_94f89715_x002D_e097_x002D_4e8b_x002D_ba79_x002D_ea02aa8b7adb_x007D_="Lists/MyList/2_.000" PubDate="Wed, 02 Nov 2011 00:32:47 GMT" FileExtension="" FileSize="" DocumentIconImageUrl="/_layouts/IMAGES/icgen.gif" LinkUrl="http://localhost/Lists/MyList/2_.000" Style="Default" GroupStyle="DefaultHeader" __begincolumn="False" __begingroup="False"></Row>
    <Row Title="Item 1" FileRef="/Lists/MyList/1_.000" Author="George Lucas" Editor="George Lucas" FSObjType="0" PermMask="0x400000300c231061" ProgId="" File_x005F_x0020_Type="" File_x005F_x0020_Size="" PublishingRollupImage="" Comments="" ID="1" Modified="2011-11-02 11:32:30" Created="2011-11-02 11:32:30" _Level="1" WebId="bf499d62-67d5-46af-9746-acd74885fa45" ListId="19877173-9e7a-4543-962b-c4dc4261aceb" _x007B_94f89715_x002D_e097_x002D_4e8b_x002D_ba79_x002D_ea02aa8b7adb_x007D_="Lists/MyList/1_.000" PubDate="Wed, 02 Nov 2011 00:32:30 GMT" FileExtension="" FileSize="" DocumentIconImageUrl="/_layouts/IMAGES/icgen.gif" LinkUrl="http://localhost/Lists/MyList/1_.000" Style="Default" GroupStyle="DefaultHeader" __begincolumn="False" __begingroup="False"></Row>
  </Rows>
</dsQueryResponse>

Using Reflection to set web part properties

Think about this scenario: you are tasked to do develop a custom SharePoint web part with a few custom properties. This custom web part will be removed and added back to a page during feature activation during a typical solution patch deployment process.
The requirement dictates that web part properties are preserved instead of being reset to their default value after this patch is deployed.

I tried to make a general solution to this by not worrying about the Type of the web part of which the properties we want to preserve. This makes use of Reflection in order to retrieve and set the web part properties. When I implement my web parts, I typically group custom properties in category groups by decorating the property with System.ComponentModel.CategoryAttribute attribute.

For example:

[System.Web.UI.WebControls.WebParts.WebBrowsable(true),
System.ComponentModel.Category("Parameters"),
Microsoft.SharePoint.WebPartPages.FriendlyName("Header Text"),
System.Web.UI.WebControls.WebParts.Personalizable(System.Web.UI.WebControls.WebParts.PersonalizationScope.Shared),
System.Web.UI.WebControls.WebParts.WebDisplayName("Header Text"),
System.Web.UI.WebControls.WebParts.WebDescription("Header text for the grid")]
public string HeaderText { get; set; }

Right before you remove the old web part, use the following method to collect properties using reflection:

private Dictionary<string, object> CollectExistingProperties(System.Web.UI.WebControls.WebParts.WebPart existingWp)
{
    Dictionary<string, object> existingProperties = new Dictionary<string, object>();
    PropertyInfo[] properties = existingWp.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
    if (properties == null) return existingProperties;
    foreach (PropertyInfo prop in properties)
    {
        object[] myAttributes = prop.GetCustomAttributes(typeof(System.ComponentModel.CategoryAttribute), true);
        if (myAttributes != null && myAttributes.Length > 0)
        {
            for (int j = 0; j < myAttributes.Length; j++)
            {
                if (string.Compare(((System.ComponentModel.CategoryAttribute)myAttributes[j]).Category, "Parameters", true) == 0)
                {
                    existingProperties.Add(prop.Name, prop.GetValue(existingWp, null));
                }
            }
        }
    }
    return existingProperties;
}

Then, for the new web part, use the following method to set custom properties which have been ‘preserved’ earlier:

private void SetPropertyCollection(ref System.Web.UI.WebControls.WebParts.WebPart webPart, Dictionary<string, object> preserved)
{
    PropertyInfo[] runtimeProperties = webPart.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
    if (preserved != null && preserved.Count > 0 && runtimeProperties == null)
    {
        //Handle error
    }

    foreach (PropertyInfo prop in runtimeProperties)
    {
        object[] myAttributes = prop.GetCustomAttributes(typeof(System.ComponentModel.CategoryAttribute), true);
        if (myAttributes != null && myAttributes.Length > 0)
        {
            for (int j = 0; j < myAttributes.Length; j++)
            {
                if (string.Compare(((System.ComponentModel.CategoryAttribute)myAttributes[j]).Category, "Parameters", true) == 0)
                {
                    if (preserved.ContainsKey(prop.Name))
                    {
                        prop.SetValue(webPart, preserved[prop.Name], null);
                    }
                }
            }
        }
    }
}

When SPSecurityTrimmedControl AuthenticationRestrictions FAILs, simple workaround

SPSecurityTrimmedControl is a nice offering by Microsoft when it comes showing and hiding parts of your page/web part/control depending upon user permissions.

In my case, I have the requirement to show some parts of my page only to anonymous users, and some only to logged in users, hence giving the illusion of different context for logged on and logged out users while staying on the same page. I was ready to take advantage of the AuthenticationRestrictions property until I found out that it doesnt work when I give it the value of AnonymousUsersOnly

This blog entry by Waldek Mastykarz mentioned this same problem. I figured that the SPSecurityTrimmedControl class is not sealed, yeeha… that means I can extend it to fix this broken feature. I work around this by adding my own property to the extension of SPSecurityTrimmedControl class, and overriding the Render method of this class.

Simply add this class to your project:

[ParseChildren(false), Designer(typeof(SPControlDesigner)), SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true), AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal), SharePointPermission(SecurityAction.InheritanceDemand, ObjectModel = true), AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
public class SecurityTrimmer : SPSecurityTrimmedControl
{
    private bool _AnonymousOnly = false;

    public bool AnonymousOnly
    {
        get { return _AnonymousOnly; }
        set { _AnonymousOnly = value; }
    }

    [SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
    protected override void Render(HtmlTextWriter output)
    {
        if ((!AnonymousOnly && HttpContext.Current.Request.IsAuthenticated) || (AnonymousOnly && !HttpContext.Current.Request.IsAuthenticated))
        {
            base.Render(output);
        }
    }
}

Then I use this within my page by replacing the SPSecurityTrimmedControl with my own control:

<%@ Register TagPrefix="myuc" Namespace="[[Your Namespace here]]" Assembly="[[Your Assembly Name here]]"%>

<!-- your other markup -->

<myuc:SecurityTrimmer id="SecurityTrimmedControl1" runat="server" AnonymousOnly="True">
   Text visible only to anonymous users
</myuc:SecurityTrimmer>

And Voila, it works!

Enabling IntelliSense for BDC metadata

BDC metadata authoring can be a daunting task especially when we are so get used toVisual Studio’s IntelliSense. We can actually enable IntelliSense through these steps (in this case I’m using Visual Studio as my XML editor):

1. Open up the XML and click on the ellipsis in the Schemas property (bring up the property window)

2. XML Schemas window will open, click on Add

3. Navigate to BdcMetadata.xsd file, usually this file sits in: Program Files\Microsoft Office Servers\12.0\Bin  folder

If you are authoring BDC metadata outside of Sharepoint Dev box, copy the xsd file out and refer to the location where you copied the file into. In my case, I like to leave my Sharepoint dev VPC running while doing some development on my Vista physical machine, so I copied the BdcMetadata.xsd file out of the VPC and put it on my physical machine.

Problem when importing Excel spreadsheet to Sharepoint List

I came to the following error message when trying to import a shreadsheet into Sharepoint List:

Method ‘Post’ of object ‘IOWSPostData’ failed

I found a post that has the solution for this.

This error is caused by an Excel add in that use an old version of post method which was there in Sharepoint Team Services 1.0. To solve this, we need to direct the excel add-in to communicate with Sharepoint using SOAP through these steps:

  1. Open up C:\Program Files\Microsoft Office\Office12\1033\EXPTOOWS.XLA
  2. Bring up the VBA window using Alt+F11
  3. Find the Initialize() method
  4. Comment out the following line:
    lVer = Application.SharePointVersion(URL)
  5. Put in the following line:
    lVer = 2
  6. Save the Excel add in and retry your import
%d bloggers like this: