Tuesday, May 11, 2010

How to change SharePoint Workflow status to a custom value

Step 1 - Add following to workflow.xml file

<ExtendedStatusColumnValues>

<StatusColumnValue>Need Restart</StatusColumnValue>                

<StatusColumnValue>Waiting for Approval</StatusColumnValue>

</ExtendedStatusColumnValues>

Step 2 - Use SharePoint Workflow SetState shape.

Bind a field workflowState to its State Property.

MethodInvoking - setWorkflowStatus_MethodInvoking

private void setWorkflowStatus_MethodInvoking(object sender, EventArgs e)

{

//// SPWorkflowStatus.Max for "Need Restart"

//// SPWorkflowStatus.Max + 1 for "Waiting for Approval"

this.workflowState = (int)SPWorkflowStatus.Max;

}

Step 3 - For an existing feature, deactivate - uninstall - install - Activate the feature for the above changes to reflect.

Friday, February 5, 2010

SiteMap Provider based Mega Menu in ASP.NET

Few days back I got a requirement to develop mega menu navigation for a SharePoint application with following requirements.

  1. The design should look like screenshot-1.
  2. The mega menu has one static level and 2 dynamic levels.
  3. It should be based on SharePoint out of box GlobalNavSiteMapProvider. So that the end user can leverage on SharePoint out of box Navigation Settings to edit it's navigation structure.
  4. The dynamic menu items should be arranged from Top to Bottom and then left to right. It means the fly out will have columns of menu items.
  5. The number of items from Top to Bottom should be configurable so that the end user can control the number of columns created in the fly out. More items you have from Top to bottom less will be the no. of columns. The argument for such a design is - the menu should adjust automatically based on the no. of items, a menu control has.




Like any developer, ASP.NET Menu control is the first thing that came to my mind to use for the above requirements. But after spending some time on the ASP.NET Menu control, I found that ASP.NET menu control doesn't support the design (Screenshot-1) we expect. While our requirement was to render the 2nd level dynamic items just under its parent, ASP.NET Menu control renders the 2nd level dynamic items fly out from left to right.

Hence the final decision was to create a Custom control in ASP.NET that will derive its items from any Sitemap Provider and render items as per the above requirements. So the first thing that we searched for - An html drop down menu that contains multiple columns of links. We found jQuery Mega Menu from Javascriptkit.com is the idle for our requirement. This html menu is really awesome. That entire jQuery Mega menu composed of a .css file and a .js file.

The steps to use Jquery Mega menu are as follows.

  1. Download following files from Javascriptkit.com and add them to your head section using link attribute.
  • jkmegamenu.css
  • jkmegamenu.js
  1. Create an anchor link (act as a static link) with id="megaanchor".
  2. Create a DIV for mega drop down menu with id="megamenu1" and class="megamenu"
  3. Initialize the menu by making following JavaScript call

jkmegamenu.definemenu("megaanchor", "megamenu1", "mouseover");

Where as

megaanchor - id of the anchor link

megamenu1 - id of the DIV that represents the mega drop down.

As the static anchor link and its mega drop down menu items are created dynamically in our Custom Control, so we had to generate the ids dynamically and make the JavaScript calls using Page.ClientScript.RegisterClientScriptBlock in OnPreRender method.

As per the demand of our requirement, we exposed following configurable properties.

NumberOfItemsPerColumn - Using this, the end user can specify the no. of items per column in drop down mega menu.

ID - Unique Id of the current instance of the Custom Control.

ProviderName - Name of the Site Map provider that the Custom Control will use to fetch menu items.

DynamicTemplateLevel1 - Template for menu item to be rendered in Dynamic level1.

DynmaicTemplateLevel2 - Template for menu item to be rendered in Dynamic level2.

The complete code of the Custom Control is given below.


/// <summary>

/// This class renders the mega menu from the given provider.
/// </summary>
[ToolboxData("<{0}:MegaMenu runat=server></{0}:MegaMenu>")]
public class MegaMenu : WebControl
{
/// <summary>
/// Maxium Dynamic Level allowed for Mega menu.
/// </summary>
private const int DynamicLevelMax = 2;

/// <summary>
/// Class level variable to track number of links rendered.
/// </summary>
private int itemsInColumnCounter;

/// <summary>
/// Stores dynamic templates as an array.
/// </summary>
private string[] templates;

/// <summary>
/// Initializes a new instance of the <see cref="NewMegaMenu"/> class.
/// </summary>
public MegaMenu()
{
this.NumberOfItemsPerColumn = 1000;
}

/// <summary>
/// Gets the max dynamic level allowed in Mega menu.
/// </summary>
/// <value>The max dynamic level.</value>
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public static int MaxDynamicLevel
{
get
{
return DynamicLevelMax;
}

}

/// <summary>
/// Gets or sets the number of items per column.
/// </summary>
/// <value>The number of items per column.</value>
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("0")]
[Localizable(true)]
public int NumberOfItemsPerColumn
{
get;
set;

}

/// <summary>
/// Gets or sets the Mega menu id.
/// </summary>
/// <value>The id that uniquely identify each Mega menu instance.</value>
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public string Id
{
get;
set;
}

/// <summary>
/// Gets or sets the Mega menu dynamic level.
/// </summary>
/// <value>The dynamic level.</value>
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public int DynamicLevel
{
get;
set;
}

/// <summary>
/// Gets or sets the name of the provider.
/// </summary>
/// <value>The name of the provider.</value>
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public string ProviderName
{
get;
set;
}

/// <summary>
/// Gets or sets the dynamic template level1.
/// </summary>
/// <value>The dynamic template level1.</value>
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public string DynamicTemplateLevel1
{
get;
set;
}

/// <summary>
/// Gets or sets the dynamic template level2.
/// </summary>
/// <value>The dynamic template level2.</value>
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public string DynamicTemplateLevel2
{
get;
set;
}

/// <summary>
/// Raises the <see cref="E:System.Web.UI.Control.PreRender"/> event.
/// </summary>
/// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
protected override void OnPreRender(EventArgs e)
{
try
{
base.OnPreRender(e);

if (string.IsNullOrEmpty(this.ProviderName))
{
throw new ArgumentNullException(this.ProviderName, "Mega menu parameter 'Provider Name' cannot be null");
}

if (string.IsNullOrEmpty(this.Id))
{
throw new ArgumentNullException(this.Id, "Mega menu Id cannot be null");
}

SiteMapProvider portalSiteMapProvider = SiteMap.Providers[this.ProviderName];
SiteMapNode startNode = portalSiteMapProvider.RootNode;
if (!startNode.HasChildNodes)
{
startNode = portalSiteMapProvider.RootNode;
}

int linkCount = startNode.ChildNodes.Count;

for (int counter = 0; counter < linkCount; counter++)
{

if (startNode.ChildNodes[counter].ChildNodes.Count > 0)
{
if (!this.Page.ClientScript.IsClientScriptBlockRegistered(this.Id + "_megamenu" + counter))
{
string scriptTag = string.Format(
CultureInfo.CurrentCulture,
"<script>jkmegamenu.definemenu('{0}_megaanchor{1}', '{0}_megamenu{1}', 'mouseover')</script>",
this.Id, counter);
this.Page.ClientScript.RegisterClientScriptBlock(
typeof(Page), this.Id + "_megamenu" + counter, scriptTag);

}
}
}
}
catch (Exception exception)
{
exception.LogError();
throw;
}

}

/// <summary>
/// Renders the contents of the mega menu control to the specified writer.
/// </summary>
/// <param name="writer">A <see cref="T:System.Web.UI.HtmlTextWriter"/> that represents the output stream to render HTML content on the client.</param>
protected override void RenderContents(HtmlTextWriter writer)
{
try
{
if (DynamicLevel > MaxDynamicLevel)
{
throw new ArgumentException("Dynamic level of the Mega menu cannot exceed maximum level " + MaxDynamicLevel);
}

SiteMapProvider portalSiteMapProvider = SiteMap.Providers[this.ProviderName];
SiteMapNode startNode = portalSiteMapProvider.RootNode;
if (!startNode.HasChildNodes)
{
startNode = portalSiteMapProvider.RootNode;
}

templates = new string[MaxDynamicLevel];
templates[0] = this.DynamicTemplateLevel1;
templates[1] = this.DynamicTemplateLevel2;

RenderNavigation(startNode, writer);
}
catch (Exception exception)
{
exception.LogError();
throw;
}
}

/// <summary>
/// Renders the navigation.
/// </summary>
/// <param name="startNode">The start node.</param>
/// <param name="output">The output.</param>
private void RenderNavigation(SiteMapNode startNode, HtmlTextWriter output)
{
int counter = 0;

output.AddAttribute(HtmlTextWriterAttribute.Border, "0");
output.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "0");
output.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "0");
output.RenderBeginTag(HtmlTextWriterTag.Table);
output.RenderBeginTag(HtmlTextWriterTag.Tr);

foreach (SiteMapNode node in startNode.ChildNodes)
{
output.RenderBeginTag(HtmlTextWriterTag.Td);

output.AddAttribute(HtmlTextWriterAttribute.Class, "ms-topnav zz1_TopNavigationMenu_4");
output.AddAttribute(HtmlTextWriterAttribute.Border, "1");
output.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "0");
output.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "1");
output.RenderBeginTag(HtmlTextWriterTag.Table);
output.RenderBeginTag(HtmlTextWriterTag.Tr);
output.AddAttribute(HtmlTextWriterAttribute.Style, "white-space:nowrap;");
output.RenderBeginTag(HtmlTextWriterTag.Td);
RenderStaticNavigation(node, output, counter);
counter++;
itemsInColumnCounter = 0;
output.RenderEndTag();
output.RenderEndTag();
output.RenderEndTag();

output.RenderEndTag();

}
output.RenderEndTag();
output.RenderEndTag();

}

/// <summary>
/// Renders the static navigation i.e. the top most menus.
/// </summary>
/// <param name="parentNode">The parent node.</param>
/// <param name="output">The output.</param>
/// <param name="staticMenuCounter">The static menu counter.</param>
private void RenderStaticNavigation(SiteMapNode parentNode, HtmlTextWriter output, int staticMenuCounter)
{
int itemCount = 0;
bool isLastColumnDIVClosed = true;

if (parentNode.ChildNodes.Count > 0)
{
output.AddAttribute(HtmlTextWriterAttribute.Href, parentNode.Url);
output.AddAttribute(HtmlTextWriterAttribute.Id, this.Id + "_megaanchor" + staticMenuCounter);
output.AddAttribute(HtmlTextWriterAttribute.Class, "zz1_TopNavigationMenu_1 ms-topnav zz1_TopNavigationMenu_3");
output.RenderBeginTag(HtmlTextWriterTag.A);
output.Write(parentNode.Title);
output.RenderEndTag();
output.AddAttribute(HtmlTextWriterAttribute.Class, "megamenu");
output.AddAttribute(HtmlTextWriterAttribute.Id, this.Id + "_megamenu" + staticMenuCounter);
output.RenderBeginTag(HtmlTextWriterTag.Div);

output.AddAttribute(HtmlTextWriterAttribute.Class, "column");
output.RenderBeginTag(HtmlTextWriterTag.Div);
isLastColumnDIVClosed = false;

foreach (SiteMapNode node in parentNode.ChildNodes)
{
isLastColumnDIVClosed = RenderDynamicNode(node, output, itemCount, 1, isLastColumnDIVClosed);
itemCount++;
}

if (!isLastColumnDIVClosed)
{
output.RenderEndTag();
isLastColumnDIVClosed = true;
}
output.RenderEndTag();
}
else
{
output.AddAttribute(HtmlTextWriterAttribute.Href, parentNode.Url);
output.AddAttribute(HtmlTextWriterAttribute.Class, "zz1_TopNavigationMenu_1 ms-topnav zz1_TopNavigationMenu_3");
output.RenderBeginTag(HtmlTextWriterTag.A);
output.Write(parentNode.Title);
output.RenderEndTag();
}

}

/// <summary>
/// Renders the dynamic node.
/// </summary>
/// <param name="parentNode">The parent node.</param>
/// <param name="output">The output.</param>
/// <param name="itemCount">The item count.</param>
/// <param name="level">The level.</param>
/// <param name="isLastColumnDIVClosed">if set to <c>true</c> [is last column DIV closed].</param>
/// <returns>boolean value if the last div for a new column is closed.</returns>
private bool RenderDynamicNode(SiteMapNode parentNode, HtmlTextWriter output, int itemCount, int level, bool isLastColumnDIVClosed)
{
if (level <= DynamicLevel)
{
if (itemsInColumnCounter >0 && itemsInColumnCounter > this.NumberOfItemsPerColumn)
{
if (!isLastColumnDIVClosed)
{
output.RenderEndTag();
isLastColumnDIVClosed = true;
}
output.AddAttribute(HtmlTextWriterAttribute.Class, "column");
output.RenderBeginTag(HtmlTextWriterTag.Div);
itemsInColumnCounter = 0;
isLastColumnDIVClosed = false;
}
else if ((itemsInColumnCounter > 0) && (parentNode.ChildNodes.Count + itemsInColumnCounter + 1 > this.NumberOfItemsPerColumn))
{
if (!isLastColumnDIVClosed)
{
output.RenderEndTag();
isLastColumnDIVClosed = true;
}
output.AddAttribute(HtmlTextWriterAttribute.Class, "column");
output.RenderBeginTag(HtmlTextWriterTag.Div);
itemsInColumnCounter = 0;
isLastColumnDIVClosed = false;
}

if (!string.IsNullOrEmpty(templates[level - 1]))
{
if (itemsInColumnCounter > 0)
{
output.Write("<br style='clear: left' />");
}
string menuitem = templates[level - 1];
menuitem = menuitem.Replace("[LINK]", parentNode.Url);
menuitem = menuitem.Replace("[TITLE]", parentNode.Title);
output.Write(menuitem);
itemsInColumnCounter++;
}

output.RenderBeginTag(HtmlTextWriterTag.Ul);

foreach (SiteMapNode node in parentNode.ChildNodes)
{
itemsInColumnCounter++;
if (node.ChildNodes.Count > 0)
{
RenderDynamicNode(node, output, itemCount, level + 1, isLastColumnDIVClosed);
}
else
{
if (level + 1 <= DynamicLevel)
{
if (!string.IsNullOrEmpty(templates[level]))
{
output.RenderBeginTag(HtmlTextWriterTag.Li);
string menuitem = templates[level];
menuitem = menuitem.Replace("[LINK]", node.Url);
menuitem = menuitem.Replace("[TITLE]", node.Title);
output.Write(menuitem);
output.RenderEndTag();
}
}
}

}

itemCount = 0;
output.RenderEndTag();
}

return isLastColumnDIVClosed;
}
}


sample.sitemap file in _layouts folder

Web.Config Entry





...
...



....
....






Use the Mega menu inside MasterPage






Tuesday, February 2, 2010

EventLog based Error Logging using ELMAH

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Xml;
using System.IO;
using System.Text;
using Elmah;
using System.Diagnostics;


namespace Sample.SampleSystems.Sales.Web.Helpers
{
/// <summary>
/// This class extends Elmah ErrorLog class to log errors to EventLog.
/// </summary>
public class EventLogErrorLog : Elmah.ErrorLog
{
string _eventLog = string.Empty;
string _eventSource = string.Empty;

public string EventLogName
{
get
{
return _eventLog;
}
set
{
_eventLog = value;
}
}

public string EventSourceName
{
get
{
return _eventSource;
}
set
{
_eventSource = value;
}
}

public EventLogErrorLog(string eventLog, string eventSource)
{
_eventLog = eventLog;
_eventSource = eventSource;
}


public EventLogErrorLog(System.Collections.IDictionary config)
{
string eventLog = Mask.NullString(config["eventLog"] as string);

if (eventLog.Length == 0)
{
eventLog = Mask.NullString(config["EventLog"] as string);

if (eventLog.Length == 0)
throw new Elmah.ApplicationException("EventLog is missing for the EventLog-based error log.");
}

string eventSource = Mask.NullString(config["eventSource"] as string);

if (eventSource.Length == 0)
{
eventSource = Mask.NullString(config["EventSource"] as string);

if (eventSource.Length == 0)
throw new Elmah.ApplicationException("EventLog is missing for the EventLog-based error log.");
}
_eventLog = eventLog;
_eventSource = eventSource;
}

public override string Name
{
get { return "iMAD EventLog Error Log"; }
}

public override ErrorLogEntry GetError(string id)
{
ErrorLogEntry logEntry = null;
if (EventLog.SourceExists(_eventSource))
{
// Create an EventLog instance and assign its source.
EventLog imadLog = new EventLog(_eventLog);
foreach (EventLogEntry entry in imadLog.Entries)
{
XmlTextReader reader = new XmlTextReader(new StringReader(entry.Message));
while (reader.IsStartElement("error"))
{
string errorid = reader.GetAttribute("errorId");

if (errorid == id)
{
Error error = ErrorXml.Decode(reader);
logEntry = new ErrorLogEntry(this, errorid, error);
break;
}

reader.Read();

}
reader.Close();
if (logEntry != null)
break;
}
}
return logEntry;

}

public override int GetErrors(int pageIndex, int pageSize, System.Collections.IList errorEntryList)
{
if (EventLog.SourceExists(_eventSource))
{
// Create an EventLog instance and assign its source.
EventLog imadLog = new EventLog(_eventLog);

if (imadLog.Entries.Count > 0)
{
for (int counter = pageIndex; counter < pageSize; counter++)
{
if (counter >= imadLog.Entries.Count)
{
break;
}
EventLogEntry entry = imadLog.Entries[counter];
XmlTextReader reader = new XmlTextReader(new StringReader(entry.Message));

while (reader.IsStartElement("error"))
{
string id = reader.GetAttribute("errorId");
Error error = ErrorXml.Decode(reader);
errorEntryList.Add(new ErrorLogEntry(this, id, error));
}

reader.Close();
}
}
return errorEntryList.Count;
}

return default(int);
}

public override string Log(Error error)
{
string errorId = Guid.NewGuid().ToString();
StringWriter swritter = new StringWriter();
XmlTextWriter xwriter = new XmlTextWriter(swritter);
xwriter.Formatting = Formatting.Indented;
xwriter.WriteStartDocument();
xwriter.WriteStartElement("error");
xwriter.WriteAttributeString("errorId", errorId.ToString());
ErrorXml.Encode(error, xwriter);
xwriter.WriteEndElement();
xwriter.Flush();
xwriter.Close();

// Make sure the Eventlog Exists
if (EventLog.SourceExists(_eventSource))
{
// Create an EventLog instance and assign its source.
EventLog imadLog = new EventLog(_eventLog);
imadLog.Source = _eventSource;
string messageString = swritter.ToString();
if (messageString.Length > 32765)
{
////Max limit of characters that EventLog allows for an event is 32766.
messageString = messageString.Substring(0, 32765);
}
// Write the error entry to the event log.
imadLog.WriteEntry(messageString, EventLogEntryType.Error);
}

return errorId;

}
}

internal sealed class Mask
{
public static string NullString(string s)
{
return s == null ? string.Empty : s;
}

public static string EmptyString(string s, string filler)
{
return Mask.NullString(s).Length == 0 ? filler : s;
}

private Mask() { }
}
}