Tuesday, 19 February 2008

Fix For Bug With CustomAction (with source code)

I recently overheard a colleague discussing an issue with using CustomAction elements to extend the UI in MOSS, details of his issue can be read here.

http://chrissyblanco.blogspot.com/2007/12/bug-with-customaction-in-actions-menu.html

To sum up, if you define a CustomAction as part of a feature and use tokens in the URL e.g. <UrlAction Url="~site/Test.aspx?List={ListId}"/>, the custom action will work fine in a document library. However, if you add a web part representing the same document library to another page the tokens in the url do not get replaced. This is a very irratating bug.

The fix.
The only way I could see to fix this, was to replace the tokens using javascript. The menu options are available from the dom and the webpart also creates a context object in javascript to represent the displayed list.

The challenge.
To fix every occurance in a web app.

The idea.
Control Adapters. I simple idea but it presented a few difficulties to get this working in SharePoint.

The solution.
The SharePoint solution contains a WebApplication feature that when activated submits a JobDefinition to modify the compat.browser file already provisioned in the web app. The modification is a single control adapter entry to render all listviewwebparts using my control adapter.

The control adapter then allows the listviewwebpart to render as normal and then renders some javascript afterwards that fixes the rendered listviewwebpart.

Download the solution here seed.hf1.wsp. After installing and deploying the solution, run

stsadm -o activatefeature -name seed.hf1 -url <target url>

Download the source code here seed.hf1.zip

More
I am planning to blog in more detail about the techniques used in this solution, but I want to break them down into a small series of more detailed blogs. In the meantime, have a look through my source code and please leave any question or comments.

Friday, 30 November 2007

Applying a theme to SPLongOperation

At some point in SharePoint development you'll probably write some code that takes a long time to execute and you'll run into this fella, the SPLongOperation.

Unfortunately though, you may feel let down when you run your code and the page displayed is not branded like the rest of your site.

This is because no theme is applied in the html output, but don't despair, help is at hand. The property LeadingHTML is exactly that, HTML, so I used it to inject a stylesheet link element that references the stylesheet for the current theme.


string style = @"<link rel=""stylesheet"" type=""text/css"" href=""" + currentWeb.ThemeCssUrl + @""" />";

SPLongOperation op = new SPLongOperation(this);
op.LeadingHTML = style + "[Enter your message here]";
op.Begin();



After doing this I noticed a flicker at the top of the screen. This was because of the comment tags being rendered by the SPLongOperation. To fix this I also included a style element to hide the element that flickered.


string style = @"<link rel=""stylesheet"" type=""text/css"" href=""" + currentWeb.ThemeCssUrl + @""" /><style>td.ms-globalbreadcrumb {display:none;visibility:hidden;}</style>";

Wednesday, 14 November 2007

RSViewer WebPart and Managed Urls

I have recently been developing a SharePoint solution that runs Reporting Services in SharePoint integration mode. I have a Site Definition for provisioning site collections and found that when I created a page with a ListView WebPart and connected it to a Reporting Services Report Viewer WebPart I received the following message in the report viewer web part.


The item 'http://www.mysite.com/[managedurl]/[SiteCollectionUrl]/[managedurl]/[SiteCollectionUrl]/[ReportLibrary]/MyReport.rdl' cannot be found. (rsItemNotFound)

I quickly found that this is because the list view web part provides a a datarowview with a column called DocUrl that contains the server relative url to the document. But on consumption, the report viewer wewbpart prefixes this url with the site collection's url.


Example

I have a report library called "My Reports" that contains a report called "Report1.rdl". When on the root of the web application the site collection url is "http://www.mysite.com/" and the server relative url of the document is "/My Reports/Report1.rdl". The report view web part adds them together to get.

http://www.mysite.com/My Reports/Report1.rdl - That's fine

Now lets consider the effect of the site collection being on a managed url.

The site collection url would become "http://www.mysite.com/sites/site1" the server relative url of te report would be "/sites/site1/my reports/report1.rdl". Put them together

http://www.mysite.com/sites/site1/sites/site1/My Reports/Report1.rdl - ooops



To fix this I created a web part to act as a proxy between the ListView web part and the RS Viewer WebPart.


Code

using System;
using System.Runtime.InteropServices;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Serialization;

using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.WebPartPages;
using System.ComponentModel;
using System.Data;
using System.Collections.Generic;

public class MyWebPart : System.Web.UI.WebControls.WebParts.WebPart, IWebPartRow
{
    Queue callbackQueue = new Queue();
    object rowData;
    PropertyDescriptorCollection rowSchema;

    public MyWebPart()
    {
        this.ExportMode = WebPartExportMode.All;
    }

    protected override void CreateChildControls()
    {
        return;
    }

    [ConnectionConsumer("AttrReportPathRowConsumer", "ReportDefinition", AllowsMultipleConnections = false)]
    public void SetRowProvider(IWebPartRow reportPathProvider)
    {
        if (reportPathProvider != null)
        {
            reportPathProvider.GetRowData(new RowCallback(this.OnReportPathFromProvider));
        }
    }

    [ConnectionProvider("Row", "blagh", typeof(DataFormProviderConnectionPoint), AllowsMultipleConnections = true)]
    public IWebPartRow GetProviderInterface()
    {
        return this;
    }

    protected override void RenderContents(HtmlTextWriter writer)
    {
        if (this.rowData != null)
        {
            DataRowView view = this.rowData as DataRowView;
            if (view != null)
            {
                int index = view.Row.Table.Columns.IndexOf("DocUrl");
                if (index != -1)
                {
                    writer.Write(view.Row.ItemArray[index].ToString());
                }
            }
        }
    }

    private void OnReportPathFromProvider(object rowData)
    {
        DataRowView view = rowData as DataRowView;
        if (((view != null) && (view.Row != null)) && ((view.Row.ItemArray != null) && (view.Row.ItemArray.Length > 0)))
        {
            int index = view.Row.Table.Columns.IndexOf("DocUrl");
            if (index != -1)
            {
                if (!SPContext.Current.Site.ServerRelativeUrl.Equals("/"))
                {
                    string url = SPContext.Current.Web.Site.ServerRelativeUrl;
                    string serverRelativeReportPathUrl = view.Row.ItemArray[index].ToString();
                    string siteRelativeReportPathUrl = serverRelativeReportPathUrl.Remove(0, url.Length);
                    view.Row[index] = siteRelativeReportPathUrl;
                    view.Row.AcceptChanges();
                }
            }
        }
        this.rowData = view;
        this.rowSchema = null;
        sendRow();
    }

    private void sendRow()
    {
        while (callbackQueue.Count > 0)
        {
            RowCallback callback = callbackQueue.Dequeue();
            callback(this.rowData);
        }
    }


    #region IWebPartRow Members

    void IWebPartRow.GetRowData(RowCallback callback)
    {
        if (this.rowData != null)
        {
            callback(this.rowData);
        }
        else
        {
            callbackQueue.Enqueue(callback);
        }
    }

    PropertyDescriptorCollection IWebPartRow.Schema
    {
        get
        {
            if (this.rowSchema == null)
            {
                try
                {
                    this.rowSchema = TypeDescriptor.GetProperties(this.rowData);
                }
                catch (Exception)
                {
                    return new PropertyDescriptorCollection(null);
                }
            }

            return this.rowSchema;
        }

    }

    #endregion
}