Tuesday, September 23, 2008

A more complicated SharePoint web part examples in F# with RSS Viewer and Feed List

I still up to my old tricks with learning new Microsoft Technologies by using F# as the implementation language. I find that by using a different programming language than the one in the book forces me to think through the author's implementation while I transliterate the code into F#. Other bonuses include learning to program in F# and in the process, I often trigger errors while learning the new technology that serendipitously provide me opportunites to explore the technologies in depth.

Lately, I've been exploring SharePoint by going through the book Inside Microsoft Windows SharePoint Services 3.0 by Ted Pattison and Daniel Larson and working through the code examples in Chapter 4 of the book, specifically the RSS Viewer Web Part and the Feed List Web Part example.

In the previous blog, I mentioned that I had to write a SPListCollectionAdapter in C# to wrap SPListCollection so I can use it like standard sequences in F#. I figured out a F# workaround for it so that I don't have to switch back and forth to resolve the problem. In the implementation of the F# solution, I found myself getting really annoyed by the SharePoint libray designers in not implementing the IEnumerable interface in SPBaseCollection so that I don't need to write the adapter in the first place. The second major issue with the SPBaseCollection is that it did not specify and overridable Item property and left it to the implementation class to arbitrarily define the access to the item in the collection list. I have no idea why the SharePoint library designers did this. So instead of writing the following function that can be take any subclass of SPBaseCollection:


let toSeq (splist: #SPBaseCollection) =
seq { for i in 0 .. (splist.Count-1) -> splist.get_Item(i)}

I'm now forced to write an adapter for each of the SPBaseCollection subclasses as shown in the following:

    let SPListToSeq (splist:SPListCollection) =
seq { for i in 0 .. (splist.Count-1) -> splist.get_Item(i)}

let SPWebToSeq (splist:SPWebCollection) =
seq { for i in 0 .. (splist.Count-1) -> splist.get_Item(i)}

In the implementation and testing process, I ran into problems and SharePoint showed some very unhelpful error messages. I found it invaluable to change SharePoint Web.config debug settings as outline in Jesse's SharePoint Blog. Combining that with a custom file logger, I managed to identify all my bugs and resolved the issues.

Here's a screenshot of the implemented RSS Viewer Web Part implemented in F# with the feed url pointed to F# Planet...

Here's a screenshot of the implemented Feed List Web Part that is connected to the RSS Viewer Web Part

In the F# implementation, probably the most notable change is implementation of AddLists member function in FeedListWebPart class as getSPLists in the example F# code. Instead of iterating through each SPList in the collection and using the if statement to determine whether we should add the SPList to the newly created collection, the F# version takes the entire list and pipleline it through a series of filter function to get the file list. F# helped me to think of operations at the granularity of the list level instead of at the items level. Here's the implementation in F#:


#light
namespace DemoWebParts

open System
open System.Collections.Generic
open System.ComponentModel
open System.Data
open System.IO
open System.Net
open System.Reflection
open System.Web
open System.Web.UI
open System.Web.UI.WebControls
open System.Web.UI.WebControls.WebParts
open System.Xml
open System.Xml.Xsl
open Microsoft.SharePoint.WebControls
open Microsoft.SharePoint
open Microsoft.SharePoint.Utilities

// This is a utility debugging tool that I've used to dump deugging info to a file.
// I can simple call it as Log.dump "Error messages"
module Log =

let dump (msg:string) =
using (File.AppendText(@"c:\logs\sharepoint.log"))
(fun writer -> writer.WriteLine(msg))

module SPCollectionUtility =
// This is what I would like to do but could not due to
// the design of SPBaseCollection
(*
let toSeq (splist: #SPBaseCollection) =
seq { for i in 0 .. (splist.Count-1) -> splist.get_Item(i)}
*)

let SPListToSeq (splist:SPListCollection) =
seq { for i in 0 .. (splist.Count-1) -> splist.get_Item(i)}

let SPWebToSeq (splist:SPWebCollection) =
seq { for i in 0 .. (splist.Count-1) -> splist.get_Item(i)}

module WebPartResources =
let GetNamedResource (reference:obj) (filename:string) =
use stream = filename |> Assembly.GetExecutingAssembly().GetManifestResourceStream
use reader = new StreamReader(stream)
reader.ReadToEnd()

let GetNamedResourceStream (reference:obj) (filename:string) =
filename
|> Assembly.GetExecutingAssembly().GetManifestResourceStream

type RenderMode =
| Full
| Titles

module RenderModeUtility =

// Helper function for RenderMode
let label mode =
match mode with
| Full -> "Full"
| Titles -> "Titles"

let setmode label =
match label with
| "Full" -> Full
| "Titles" -> Titles
| _ -> failwith "value must be Full or Titles to be RenderMode"


type RssViewEditorPart() = class
inherit EditorPart()

let mutable (txtXmlUrl:TextBox) = null

let mutable (lstHeadlineMode:RadioButtonList) = null


do base.Title <- "RSS View Custom Editor"

override this.CreateChildControls() =
txtXmlUrl <- new TextBox(Width=new Unit("100%"),
TextMode=TextBoxMode.MultiLine,
Rows = 3)

// Add controls to the radio button list
lstHeadlineMode <- new RadioButtonList()
(RenderModeUtility.label RenderMode.Full) |> lstHeadlineMode.Items.Add
(RenderModeUtility.label RenderMode.Titles) |> lstHeadlineMode.Items.Add

new LiteralControl("Feed Url:<br/>") |> this.Controls.Add

txtXmlUrl |> this.Controls.Add
match this.WebPartManager.Personalization.Scope with
| PersonalizationScope.User -> txtXmlUrl |> this.Controls.Add
| _ -> ()

new LiteralControl("Headline Mode:<br/>") |> this.Controls.Add
lstHeadlineMode |> this.Controls.Add


override this.SyncChanges() =
this.EnsureChildControls()
let targetPart = this.WebPartToEdit :?> RssViewWebPart
let SelectedMode = targetPart.HeadlineMode
let item = lstHeadlineMode.Items.FindByText(SelectedMode)
item.Selected <- true
txtXmlUrl.Text <- targetPart.XmlUrl

override this.ApplyChanges() =
this.EnsureChildControls()
let targetPart = this.WebPartToEdit :?> RssViewWebPart
targetPart.XmlUrl <- txtXmlUrl.Text
targetPart.HeadlineMode <- lstHeadlineMode.SelectedValue
true

end
and RssViewWebPart() = class
inherit WebPart()

let mutable (xmlUrl:string) = null
let mutable headlineMode = RenderMode.Full
let mutable (exceptionDetail:string) = null
let mutable (xmlstream:Stream) = null

(*
// Did not need to implement this...
interface IWebEditable with
member this.WebBrowsableObject
with get() = box this
override this.CreateEditorParts () =
let parts = new List<EditorPart>(1);
parts.Add(new RssViewEditorPart(ID=(this.ID+"_rssViewEditor")))
new EditorPartCollection(base.CreateEditorParts(),parts)
*)


override this.CreateEditorParts () =
let parts = new List<EditorPart>(1);
let part =new RssViewEditorPart(ID=(this.ID+"_rssViewEditor"))
parts.Add(part)
new EditorPartCollection(base.CreateEditorParts(),parts)

member this.Redirect () =
this.Context.Response.Redirect(this.XmlUrl)

[<Personalizable(PersonalizationScope.Shared);WebBrowsable(false)>]
member this.XmlUrl
with get() = xmlUrl
and set value = xmlUrl <- value

[<Personalizable(PersonalizationScope.User);WebBrowsable(false)>]
member this.HeadlineMode
with get() = RenderModeUtility.label headlineMode
and set value = headlineMode <- (RenderModeUtility.setmode value)

override this.Verbs
with get() =
let verbs = new List<WebPartVerb>()
verbs.Add(new WebPartVerb(this.ID + "_ClientSideRssOpenerVerb",
sprintf "window.open('%s','RSSXML')" this.XmlUrl,
Text = "Open RSS Feed"))
verbs.Add(
let handler = new WebPartEventHandler(fun _ _ ->
match String.IsNullOrEmpty(this.XmlUrl) with
| false -> this.Redirect()
| true -> ())

new WebPartVerb(this.ID + "_ServerSideRssOpenerVerb",
handler,
Text = "View RSS Source Feed 3.0"))


new WebPartVerbCollection(base.Verbs, verbs )


override this.OnInit (e:EventArgs) =
base.OnInit(e)

this.HelpUrl <-
(this.GetType(), "help.html")
|> this.Page.ClientScript.GetWebResourceUrl

this.HelpMode <- WebPartHelpMode.Modeless

override this.OnPreRender (e:EventArgs) =
base.OnPreRender(e)

if (String.IsNullOrEmpty(this.XmlUrl)) then ()
elif this.WebPartManager.DisplayMode.AllowPageDesign then
new LiteralControl("No display while in design mode.")
|> this.Controls.Add
else
try
let req = new Uri(this.XmlUrl) |> WebRequest.CreateDefault
req.Credentials <- CredentialCache.DefaultCredentials
req.Timeout <- 10000 // 10 seconds

let beginHandler = new BeginEventHandler(fun _ _ callback state ->
req.BeginGetResponse(callback,state))

let successHandler = new EndEventHandler(fun result ->
try
xmlstream <- req.EndGetResponse(result).GetResponseStream()
with wex ->
exceptionDetail <- wex.Message)

let errorHandler = new EndEventHandler(fun _ ->
let errmsg = sprintf "The request timed out while waiting for %s" this.XmlUrl
new Label(Text=errmsg) |> this.Controls.Add)

new PageAsyncTask(beginHandler,successHandler,errorHandler,null,true)
|> this.Page.RegisterAsyncTask

with :? System.Security.SecurityException ->
let errmsg = "Permission denied - please set trust level to WSS_Medium."
new LiteralControl(errmsg)
|> this.Controls.Add


override this.RenderContents (writer:HtmlTextWriter) =
base.RenderContents(writer)

if exceptionDetail <> null then
writer.Write(exceptionDetail)
elif (String.IsNullOrEmpty(this.XmlUrl) || xmlstream = null) then ()
else
let transformer = new XslCompiledTransform()
let xslt = match headlineMode with
| Full -> "RSS.xslt"
| Titles -> "RssTitles.xslt"
use res = WebPartResources.GetNamedResourceStream this xslt
new XmlTextReader(res) |> transformer.Load

try
use input = new XmlTextReader(xmlstream)
use output = new XmlTextWriter(writer.InnerWriter)
(input,output) |> transformer.Transform
with e ->
writer.Write(e.Message)
if xmlstream <> null then
xmlstream.Close()
xmlstream.Dispose()


[<ConnectionConsumer("Xml URL Consumer",AllowsMultipleConnections=false)>]
member this.SetConnectionInterface (provider:IWebPartField) =
provider.GetFieldValue(new FieldCallback(fun providedUrl ->

if providedUrl = null then ()
else
let urls = (providedUrl :?> String).Split([|','|])
this.XmlUrl <- urls |> Array.to_list |> List.hd))

end


type FeedListWebPart() = class
inherit WebPart()

let mutable xmlurl = ""

let spfilters = [SPListTemplateType.Categories;
SPListTemplateType.MasterPageCatalog;
SPListTemplateType.WebPageLibrary;
SPListTemplateType.WebPartCatalog;
SPListTemplateType.WebTemplateCatalog;
SPListTemplateType.UserInformation;
SPListTemplateType.ListTemplateCatalog;]


let rec getSPLists (web:SPWeb) =
Seq.append
// e.g. foreach (SPList list in web.Lists)
//(new SPListCollectionAdapter(web.Lists)
(SPCollectionUtility.SPListToSeq web.Lists
|> Seq.filter (fun x -> x.AllowRssFeeds)
|> Seq.filter (fun x -> x.EnableSyndication)
|> Seq.filter (fun x -> List.map ((<>) x.BaseTemplate) spfilters
|> List.fold_left (&&) true)
|> Seq.filter (fun x ->
x.DoesUserHavePermissions(SPBasePermissions.ViewListItems)))


// e.g. foreach (SPWeb subweb in web.Webs)
//(new SPWebCollectionAdapter(web.Webs)
(SPCollectionUtility.SPWebToSeq web.Webs
|> Seq.filter (fun w ->
w.DoesUserHavePermissions(SPBasePermissions.ViewListItems))
|> Seq.map getSPLists |> Seq.concat)


override this.CreateChildControls() =
base.CreateChildControls()

let list = SPContext.Current.Web |> getSPLists

let view = new SPGridView(AutoGenerateColumns=false)
this.Controls.Add(
["Title";"ItemCount"]
|> List.iter (fun x ->
new BoundField(DataField=x,HeaderText=x)
|> view.Columns.Add)

view.Columns.Add(
let button = new CommandField(HeaderText="Action",
SelectText="Show RSS",
ShowSelectButton=true)
button.ControlStyle.Width <- new Unit(75.0)
button)

view.SelectedIndexChanged.Add(fun x ->
xmlurl <- view.SelectedValue.ToString())
view
)

if this.Page.IsPostBack = false then
let table = new DataTable()
["Title";"ItemCount";"XmlUrl";"ID"]
|> List.iter (fun x -> table.Columns.Add(x) |> ignore)

// Need to build DataRows & DataTable...
let buildrow (list:SPList) =
let row = table.NewRow()
row.["Title"] <- list.Title
row.["ItemCount"] <- list.ItemCount.ToString()
row.["ID"] <- list.ID
let url = sprintf "%s/_layouts/listfeed.aspx?List=%s" list.ParentWebUrl (list.ID.ToString())
row.["XmlUrl"] <-
this.Page.Request.Url.GetLeftPart(UriPartial.Authority) +
SPUtility.MapWebURLToVirtualServerURL(list.ParentWeb,url)
table.Rows.Add(row)

Seq.iter buildrow list
view.DataKeyNames <- [|"XmlUrl"|]
view.DataSource <- table
view.DataBind()


[<WebBrowsable(true);
Category("Configuration");
Personalizable(PersonalizationScope.User);
DefaultValue("");
WebDisplayName("Xml Url");
WebDescription("F# RSS Feed XML URL")>]
member this.XmlUrl
with get() = xmlurl
and set value =
if String.IsNullOrEmpty(xmlurl) then
let uri = new Uri(value)
xmlurl <- uri.AbsolutePath
else
xmlurl <- null

interface IWebPartField with
member this.Schema
with get() =
TypeDescriptor.GetProperties(this).Find("XmlUrl",false)

member this.GetFieldValue (callback:FieldCallback) =
(this :> IWebPartField).Schema.GetValue(this)
|> callback.Invoke


[<ConnectionProvider("XmlUrl Provider")>]
member this.GetConnectionInterface() = this :> IWebPartField

end

[<Assembly: System.Reflection.AssemblyVersion("1.0.0.0")>]
[<Assembly: System.Security.AllowPartiallyTrustedCallersAttribute>]
do()

No comments: