Wednesday, March 23, 2011

Creating a Custom Validator Control in .Net

The ASP .Net has its own form validation framework. This framework consists of:
  • Base Validation Class, the BaseValidator
  • ASP Validation Controls
  • A Clientside Validation JavaScript Framework
The "Base Validation Class" is the base of all validation controls and is also used to create own custom controls. If you want to create your own validation control, you have to inherit from the base class.

The ASP validation controls are basic validation controls provided by Microsoft. These are:
  • RequiredFieldControl: Checks an input field if its empty
  • CompareValidator: Compares the value of a field with another value. This can be a value in another field or a constant value.
  • RangeValidator: Checks if the value in a form field is within a range
  • RegularExpressionValidator: RegularExpression validators are used to check if the value in a control matches a pattern defined by the regular expression.
  • CustomValidator: If none of the Validators are useful for a purpose, you can create a custom validator. You must handle a customvalidator in the code-behind of your site (aspx page). This means you write backend code for the validator.
All validation controls can validate a field in the frontend or in the backend. Therefore they need also an implementation in JavaScript. Microsoft provides a JavaScript solution for its own validation controls.

Creating a Custom Validation Control

When I talk about custom validation controls, I don't mean the standard "CustomValidator" or an implementation of it. I mean an independent validator like the reqularexpressionvalidator for a specific purpose, which can be used in multiple pages. As an example you want to create a date validator which has attributes to force the user to enter only dates in the past or in the future.

Before you begin to write code you should know that you have to write:
  • Logic for the Backend Validation
  • Logic for the Frontend Validation. This must correspond 1to1 to the backend logic.
  • Logic for the Frontend Validation Framework. The validation framework must know which code to use for validation when a custom validation control is created.

Understanding the client validation

As an example we have two form fields to validate. The first field is a date field and the second one a text field. The value of the date field must be in a date format whereas the text field should not be empty. Here is the simple code: (The date validation is not correct, it is just an example)

 <asp:Label ID="lblBirth" Text="Birthday:" runat="server" /><br />  
 <asp:TextBox ID="txtDate" Width="80" MaxLength="10" runat="server" />     
 <asp:RegularExpressionValidator runat="server"  
           ErrorMessage="Please check your date format (19.05.2001)"  
           ValidationExpression="\d\d\.\d\d\.\d\d\d\d" />  
 <div class="spacer"></div>  
 <asp:Label ID="lblName" Text="Name:" runat="server" /><br />  
 <asp:TextBox ID="txtName" Width="200" runat="server" />  
 <asp:RequiredFieldValidator runat="server"  
           ErrorMessage="Please enter a name"  
           EnableClientScript="true" />  

The clientvalidation is enabled, so the validation is made in the browser. But how is this done?
When you insert a validation control into your aspx page the validator inserts JavaScript Code
to the end of you site. When you look at the page source you'll find something like this:

 var Page_Validators = new Array(document.getElementById("ctl02"), document.getElementById("ctl03"));  

 var Page_Validators = new Array(document.getElementById("ctl02"), document.getElementById("ctl03"));  
 var ctl02 = document.all ? document.all["ctl02"] : document.getElementById("ctl02");  
 ctl02.controltovalidate = "txtDate";  
 ctl02.errormessage = "Please check your date format (19.05.2001)";  
 ctl02.evaluationfunction = "RegularExpressionValidatorEvaluateIsValid";  
 ctl02.validationexpression = "\\d\\d\\.\\d\\d\\.\\d\\d\\d\\d";  

The line

 var Page_Validators = new Array(document.getElementById("ctl02"), document.getElementById("ctl03"));  

creates an array "Page_Validators" that contains some DOM node. These Dom nodes are the rendered html of the validaton messages:

 <span id="ctl02" style="color:Red;visibility:hidden;">Please check your date format (19.05.2001)</span>  
 <span id="ctl03" style="color:Red;visibility:hidden;">Please enter a name</span>  

The following line creates a variable ct102 which contains a reference to the DOM nodes. In JavaScript everything is handled with references. Therefore a change in the variable ct102 changes the DOM node with the id "ct102". The variable ct102 is extended with the properties:
  • .controltovalidate: These property contains the id of the input field
  • .errormessage: This is the error message to display
  • .evaluationfunction: This is the name of the JavaScript function to call when validating. For the standard validators the functions are implemented in the JavaScript Validation Framework.
  • .initialvalue: If a field has a initial value, it is handled like the field does not contain a value.
These 4 properties "controltovalidate, errormessage, evaluationfunction and initialvalue" are used by all validators. But you can also extend these properties for your own custom validator, like the RegularExpressionValidator does with the "validationexpression" property.

When you click the send button to submit your form, a validation function "Page_ClientValidate" is called which iterates through the "Page_Validators" array and calls the function defined in the "evaluationfunction" property such as the built-in evaluation function for the RequiresFieldValidator:

 function RequiredFieldValidatorEvaluateIsValid(val) {  
   return (ValidatorTrim(ValidatorGetValue(val.controltovalidate)) != ValidatorTrim(val.initialvalue))  

These evaluation functions always return true or false. The returned value is saved in the ".isvalid" property of the validator. Also a global variable "Page_IsValid" is created which returns a boolean value. Is "Page_IsValid" valid is false the submit of the form is cancelled and the errors in the "errormessage" property of the validators are displayed.

Creating the Backend Code

When creating a custom validator you must inherit from the "BaseValidator" class. This class offers multiple properties and functions to override for creating a valid validator. The most important methods are:
  • bool ControlPropertiesValid(): You can use this function to check if the properties of your validator are valid.
  • bool EvaluateIsValid(): The real validation happens in this method.
and the most important properties:
  • ControlToValidate : ID of the control to validate
  • ErrorMessage: The error message string
  • EnableClientScript: If client validation is enabled or not

There are lot of more but these are enough for our example. As an example we create now a date validator:

1:  namespace ValidatorDemo.Validator  
2:  {  
3:  public class DateValidator : BaseValidator  
4:  {  
5:      #region Private Fields  
6:      private String mValueToValidate;  
7:      private Control mControlToValidate;  
8:      #endregion  
9:      #region Lifecycle Overrides  
10:      /// <summary>  
11:      /// Initialize the client script properties  
12:      /// </summary>  
13:      /// <param name="pE">Not used</param>  
14:      protected override void OnInit(EventArgs pE)  
15:      {  
16:          if (this.EnableClientScript)  
17:              RegisterClientValidationScript();  
18:      }  
19:      /// <summary>  
20:      /// Checks if the properties are correct  
21:      /// </summary>  
22:      /// <returns>True if properties are o.k.</returns>  
23:      protected override bool ControlPropertiesValid()  
24:      {  
25:          Control findControl = this.FindControl(this.ControlToValidate);  
26:          if (findControl is TextBox)  
27:          {  
28:              TextBox txtBox = ((TextBox)findControl);  
29:              mControlToValidate = txtBox;  
30:              mValueToValidate = txtBox.Text;         
31:              return true;  
32:          }  
33:          else  
34:          {  
35:              return false;  
36:          }  
37:      }  
38:      /// <summary>  
39:      /// Validates the date  
40:      /// </summary>  
41:      /// <returns>True if evaluation is valid, else false</returns>  
42:      protected override bool EvaluateIsValid()  
43:      {  
44:          try  
45:          {  
46:              string sDate = mValueToValidate;  
47:              if (sDate == string.Empty) return true;  
48:              CultureInfo ci = new CultureInfo("de-DE");  
49:              DateTime date = DateTime.Parse(sDate, ci);  
50:          }  
51:          catch  
52:          {  
53:              return false;  
54:          }  
55:          return true;  
56:      }  
57:      #endregion  
58:      #region Helper  
59:      /// <summary>  
60:      /// The Method provides the clientside validation script.  
61:      /// </summary>  
62:      private void RegisterClientValidationScript()  
63:      {  
64:          //Registering validator clientside javascript function  
65:          Page.ClientScript.RegisterExpandoAttribute(ClientID, "evaluationfunction", "ValidateDateBox", false);  
66:      }  
67:      #endregion  
68:  }  
69:  }  

In the method "OnInit" we check if the control property to activate the client script is set to true. If so the "RegisterClientValidationScript" method is called. It expands the DOM object with the property "evaluationfunction" and sets it to a custom JavaScript function "ValidateDateBox". The function "ValidateDateBox" must be implemented in the client code. Now following client code is generated when we insert this control:

 var ctl02 = document.all ? document.all["ctl02"] : document.getElementById("ctl02");  
 ctl02.evaluationfunction = "ValidateDateBox";  
 ctl02.controltovalidate = "txtDate";  
 ctl02.errormessage = "Enter a valid date (e.g. 05.06.1981)";  

In the "ControlPropertiesValid" method we check it the control we have to validate is a textbox control. All other controls are not accepted. If the control is a textbox we initialize to class variables to hold the controls and its value.

At least the "EvaluateIsValid" method is called in the lifecycle. It checks if the value is a correct date and returns a boolean value to indicate this.

As you can see it is very easy to create a validator control.

Now we create the client code for the validator. The function must have the name "ValidateDateBox" like how we defined it in the backend code. The evaluation function has one argument, the validator, so it can get the value to validate.

 function ValidateDateBox(val) {  
     var oDate = document.getElementById(val.controltovalidate);  
     var sDate = oDate.value;  
     // Not required field  
     if (sDate == "") {  
         return true;  
     // Check Date       
     return ParseDate(sDate);  
 function ParseDate(sDate) {  
     var iDay, iMonth, iYear;  
     var arrValues;  
     var today = new Date();      
     var fPoint = /^\d{1,2}\.\d{1,2}\.\d{4}$/;  
     if (fPoint.test(sDate)) {  
         arrValues = sDate.split(".");  
     } else {  
         return false;  
     iDay = arrValues[0];  
     iMonth = arrValues[1];  
     iYear = arrValues[2];  
     if ((iMonth == null) || (iYear == null)) {  
         return false;  
     if ((iDay > 31) || (iMonth > 12) || (iYear < 1900)) {  
         return false;  
     var dummyDate = new Date(iYear, iMonth - 1, iDay, 0, 0, 0);  
     if ((dummyDate.getDate() == iDay) && (dummyDate.getMonth() == iMonth - 1) && (dummyDate.getFullYear() == iYear)) {  
         return dummyDate;  
     return false;  

You can check the validation by entering a date like "40.11.2002" or "31.02.2001". Both should be invalid.

Extending the validator with custom properties

Now we want a property "OnlyFutureDate" that force users to enter a date in the future. This can be useful for traveling agencies or insurances. The validator now looks like that:

 <asp:Label ID="Label1" Text="Birthday:" AssociatedControlID="txtDate" runat="server" /><br />  
 <asp:TextBox ID="txtDate" Width="80" MaxLength="10" runat="server" />     
 <demo:DateValidator runat="server"  
     ErrorMessage="Enter a valid date (e.g. 05.06.1981)"  
     EnableClientScript="true" />  

In the backend code we create a private property "mOnlyFuture" and a public property "OnlyFuture". The public property is the property used in the aspx page as control property.

 private Boolean mOnlyFutureDate = false;  
 /// <summary>  
 /// Allows only dates that lie in the future  
 /// </summary>  
 public Boolean OnlyFutureDate  
     set { mOnlyFutureDate = value; }  

We extend the "EvaluateIsValid" method to check if the property is set and the date is a date in the future:

 protected override bool EvaluateIsValid()  
         string sDate = mValueToValidate;  
         if (sDate == string.Empty) return true;  
         CultureInfo ci = new CultureInfo("de-DE");  
         DateTime date = DateTime.Parse(sDate, ci);  
         DateTime now = DateTime.Now;  
         DateTime today = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0);  
         // if only future is allowed  
         if (mOnlyFutureDate)  
             return (date >= today);  
         return false;  
     return true;  

The clientcode must also be informed if the property is set or not. So we extend the DOM object with an additional property "onlyfuturedate".

 private void RegisterClientValidationScript()  
     //Registering validator clientside javascript function  
     Page.ClientScript.RegisterExpandoAttribute(ClientID, "evaluationfunction", "ValidateDateBox", false);  
     Page.ClientScript.RegisterExpandoAttribute(ClientID, "onlyfuturedate", mOnlyFutureDate.ToString().ToLower(), false);  

Following client code is generated:

 var ctl02 = document.all ? document.all["ctl02"] : document.getElementById("ctl02");  
 ctl02.evaluationfunction = "ValidateDateBox";  
 ctl02.onlyfuturedate = "true";  
 ctl02.controltovalidate = "txtDate";  
 ctl02.errormessage = "Enter a date in the future";  

At least we must change the client code, so the JavaScript can also accept the new property.

 function ValidateDateBox(val) {  
     var oDate = document.getElementById(val.controltovalidate);  
     var isOnlyFuture = val.onlyfuturedate;       
     var sDate = oDate.value;  
     // Not required field  
     if (sDate == "") {  
         return true;  
    var dummyDate = ParseDate(sDate);  
     var now = new Date();  
     var today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); // 23.09.2011 00:00:00  
     if (dummyDate != false) {  
         // Check if it's a future date  
         if (isOnlyFuture == "true") {  
             return (dummyDate >= today);  
     return (dummyDate != false);  

Because we extended the DOM object in the backend code with the property "onlyfuturedate" we can now use it to check if the property is set.

Updatepanels and Custom Validation Controls

All other validation controls are made in the form explained in this post. One thing you have to pay attention is when you use "UpdatePanels". When a custom validator sits in an updatepanel the expando attributes get lost when the panel is updated. Therefore you have to register the expando attribute in the ScriptMaganer Control. The propably easiest way to do this is to create a custom helper method, which check if a scriptmanager exists and registers than like that:

 public static void RegisterExpandoAttribute(this Control control, string controlId, string attributeName, string attributeValue, bool encode)  
     if (control.IsInsideUpdatePanel() && control.HasPageScriptManager())  
         ScriptManager.RegisterExpandoAttribute(control, controlId, attributeName, attributeValue, encode);  
         control.Page.ClientScript.RegisterExpandoAttribute(controlId, attributeName, attributeValue, encode);  

See following post for more info:

Saturday, March 19, 2011

SharePoint 2010 MySite in detail

In general
The SharePoint 2010 MySite has changed in major points to the one in SharePoint 2007. The most striking renewals are probably the tagging und activity features. The user can now tag, comment and rate his sites and documents.

Since SharePoint 2010 has no longer a Shared Service Provider the MySite is now bound to the Profile Service. This means that the profile service must be running and configured before you can create a MySite.

The MySite uses a special master page, the "mysite.master", which is activated by the feature MySiteLayouts and uploaded to the master page gallery of the MySite sitecollection. It's relatively easy to change and switch the masterpage either by hand or by feature. The sitecollection administrator can upload a masterpage to the masterpage gallery and easily switch the master.

The good news for the frontend engineer is that the new mysite.master consists mostly of div layouts, so it
is easier to style by css. But in generally it is more difficult to change the layouts than the masterpage. This is because the mysite is a team site and has no publishing features. But you can easily change that by activating
the publishing features by hand.

The easiest way to style the mysite is propably by using themes. The sitecollection admin can create his own themes or use the existing ones.

The MySite has public and private spheres. The public pages use a different site template as the personal pages

  • Public : SPSMSITEHOST (My Site Host)
  • Private : SPSPERS (SharePoint Portal Server Personal Space)

Once you click on "My Site" within the Site Actions menu, it leads to the public area, to the default.aspx MySite site collection.

Public areas have the same view for each user and can not be personalized. All users from the ProfilDB have access to this area and only the site collection administrators can customize these pages and web parts.

The public area includes the following page:

  • default.aspx
  • discusssion.aspx
  • person.aspx
  • OrganizationView.aspx
  • personcontent.aspx
  • tagprofile.aspx

The HTML structure of these pages cannot be modified by deployment. The layout of these pages can be adjusted only with CSS, Themes or SharePoint Designer.

Clicking on "My Profile" (Site Actions Menu) leads to the public profile page. This page displays the profile information of the selected users. If the user wants to see his own profile, he has the rights to change the stored profile information. The changes are written back to the Profile-DB. If a synchronization is configured with an AD the information could also be written back to the AD.

By clicking on "My Content" in the MySite global navigation the user enters his private area. His personal site is a site collection within the MySite. If it does not exist it is created the first time. A user can create lists, modifiy content and add webparts to his personal page. It is used as a private storage of documents and information.

A private site has the url "Personal" ist the managed path of the sitecollection and "mkaplan" the accountname and the sitecollection.

Looking at the global navigation, you'll realize that the MySite is logically divided into 3 parts. These are:

  1. My NewsFeed: Area for actual news from colleagues
  2. My Content: Area for personal data
  3. My Profile: Area with profile information of the MySite users

The following sections will describe these parts.

Lists on MySiteHost
The root site has following lists predefined:

List NameDescription
Customized ReportsFor Web Analytics
Form TemplatesContains the form templates
Style LibraryFor custom stylessheets
Organization PhotosOrganization logos
User PhotosProfile pictures

Personalization Sites
You can also use personalized sites witin the public areas of the MySite. This requires to create a new site using the "Personalization Site" template.

Personalization site template

"Personalization Sites" use the "Current User Filter" webpart, which can be connected with other webparts to display personalized data.

When you create a "Personalization Site" in the MySite the site will appear within the global navigation of the MySite as an extra entry, which is available for all users. A "Personalzation Site" contains 4 webpart zones: 1 on the top, 1 at the bottom and 2 in the middle.

A personalization site

Personalization Links in the global navigation
"Personalization Links" can be created to show entries in the global navigation for a specific audience. These types of links must be created in the "Profile Service Properties" und "Configure Personalization Site".

Site Definitions within the MySite


The SPSMYSITEHOST Teamsite Sitedefinition is used for all public sites within the MySite. It activates following features:

Site Features:
  • "My Site Host" in FEATURES\MySiteHost\Feature.xml.
  • "My Site Layouts" in FEATURES\MySiteLayouts\feature.xml.
Web Features:
  • "My Site Navigation" in FEATURES\MySiteNavigation\Feature.xml.
  • "Shared Picture Library for Organizations logos" in FEATURES\MySiteHostPictureLibrary\Feature.xml.
  • "Team Collaboration Lists" in \FEATURES\TeamCollab\feature.xml.
Modules: (deployed files)
  • blog.xsl : Is used by the blog webpart
  • tagprofile.aspx
  • person.aspx
  • default.aspx
  • discussion.aspx
  • OrganizationView.aspx
  • personcontent.aspx

This Sitedefinition is used by the private sites on the MySite and activates the following features:

Adds following items to the Quicklaunch menu:
  • Documents
  • Pictures
  • Libraries
Site Features:
  • My Site Layouts: Deploys mysite master and pages
  • Base WebPart Feature
Web Features:
  • ‘Personalization Site’ in FEATURES\PersonalizationSite\feature.xml.
  • ‘My Site Navigation’ in FEATURES\MySiteNavigation\Feature.xml.
  • ‘Team Collaboration Lists’ in FEATURES\TeamCollab\feature.xml.
  • public.aspx: Contains a redirect webpart
  • default.aspx
WebParts :
  • none
  • Personal Documents
  • My Documents
  • My Pictures

Globally used elements

MySite Header

The MySite Header consists of different items: (Color corresponds to the item in image)

  • Page Title / Icon : A simple HTML element
  • Global Navigation: The navigation of the MySite is editable by the site owner in the "Site Settings", "Top Link Bar".
  • Searchbox: The Searchbox is a SharePoint PeopleSearchEx Control and has properties to change the search icon, scope dropdown, frametyp or the search options.
  • HelpButton: A simple HTML element
  • MySite Menu: Can optically only be changed by styles. Menu items can be changed by feature.

MySite Header

The MySite-Header is found on every site within the MySite, in public as well as in private sites. It is bound as a Delegate Control into the mysite.master und can be found as a ascx Control in the 14\ControlTemplates folder. The name of the file is "MySiteTopNavigation". Delegate Controls in SharePoint have the ability to be easily overriden by custom controls. This means that the header of a mysite could be customized with a custom ascx control and a feature.

Business Card
Some of the public pages contain a larger area with profile informations about the user, that is called the "Business Card“. Users can see profile informations about other users and can also enter or change personal status information, like in Twitter or Facebook. There are also standard texts for the status information, which can be choosen by a dropdown. If you examine your own "Business Card" you'll find also a link under the profile picture to edit the own profile informations.

MySite Business Card

The "Business Card" is not a Control itself but a combination of HTML tables and profile informations. The profile informations are read by special profile property controls, like the departement field:

<td><SPSWC:ProfilePropertyValue PropertyName="Department" runat="server"/></td>

This means that each page containing the "Business Card" must have the same HTML code. If someone changes the layout of the Business Card with Sharepoint Designer, he must change it on every site that contains the "Business Card" to have a consistent layout.

You'll find the "Quicklaunch Navigation" directly under the "Business Card". It is embedded to the page (not master) as an ASP:Menu Control and renders an unordered list as HTML Code and styled by css to look like that:

MySite Quicklaunch

The menu navigation points can be changed within the "Site Properties" by clicking the "Quicklaunch" link under "Look and Feel". Each of the default navigation items is linked to a different aspx page:
  1. "Overview" is linked with person.aspx.
  2. "Organization" is linked with organizationview.aspx.
  3. "Content" is linked with auf personcontent.aspx.
  4. "Tags and Notes" is linked with _layouts/thoughts.aspx.
  5. "Colleagues" is linked with _layouts/MyContactLinks.aspx.
  6. "Memberships" is linked with _layouts/MyMemberships.aspx.
The first 3 pages are delivered with the MySite Feature whereas the last 3 are pages in the layouts folder. All these page are not really personalized. Therefore there need a parameter in the url to display custom content.

Detailed inspection of MySite pages

My Newsfeed - Default.aspx

The newsfeed (or activity feed) is a new feature in SharePoint 2010. This feature corresponds to the concept of news streams in facebook. It shows the latest activities of colleagues such as new tag, changed profile information or new blog posts. The page uses the "ConsolidatedNewsFeedWebPart" to show the information.
The activity feed


The tagprofile.aspx page shows information about a tag. The page is displayed when you open the "Tags & Notes" site in the quicklaunch menu and then click a tag in this site. The page contains following webparts:
  • "TagInformationWebPart" - Shows information about a tag.
  • "TaggedUrlListWebPart" - Shows tagged items.
  • "TaggedPeopleListWebPart" - Shows people that follow a tag
  • "SocialCommentWebPart" - Provides a note board
Tag information site

My Profile - Person.aspx

You get to the person.aspx when you click "My Profile" in the quicklaunch menu. This page provides an overview of general information and activities of a user.

The upper area of the site contains the "Business Card" and the "Quicklaunch Navigation". They are followed by the page content. The page layout has 4 webpart zones, like in the personal sites. The page has 1 zone in the upper,  2 in the middle and 1 at the bottom area to guarantee a flexible layout. To be flexible with webpart content, the webpart zones are located within a table layout. It is difficult to make cross browser flexible layouts only with divs.

The page contains 5 predefined webparts:

  • Ask Me About : Reads the "Ask Me About" field from the profile und displays it as a list. Informs other user about the skills of the selected user.
  • Recent Activities : Shows the recent activities of the current users, like the colleagues he added or a post he published.
  • Note Board : A textbox to post a comment.
  • Organigramm : Show the organizational structure and your position therein.
  • In Common With You : Shows common memberships and colleagues with other users.


Wednesday, March 2, 2011

How to style the SharePoint "Site Actions Menu" with Firebug

The site actions menu in SharePoint is generated automatically by JavaScript so you cannot look at the source code which classes are used to style the menu. I often use Firebug to check the classes and manipulate on the fly the css. But this is hard to do with the site actions menu because it disapears after a while.

A little trick helps here (works in SP2010 and SP2007)

Open the site in Firefox and open Firebug. Click the selection arrow and select the "Site Action" menu.

Firebug Objekt Picker

You will see a html code like show above. An <a> Tag surrounded by a <span> or <div>.

The trick is to copy the onclick code of the <span> or <div> tag. The javascript code can vary in each enviroment. The code can be easily copied from firebug or from the html code. In my case the code is:

  • In SP 2010
    CoreInvoke('MMU_Open',byid('zz8_FeatureMenuTemplate1'), MMU_GetMenuFromClientId('zz16_SiteActionsMenu'),event,false, null, 0); return false;
  • In SP 2007
    MMU_Open(byid('zz7_SiteActionsMenuMain'), MMU_GetMenuFromClientId('zz8_SiteActionsMenu'),event,false, null, 0);

Now change the "event" parameter to null:

CoreInvoke('MMU_Open',byid('zz8_FeatureMenuTemplate1'), MMU_GetMenuFromClientId('zz16_SiteActionsMenu'),null,false, null, 0); 

Open Firebug again, paste the code into the script textbox and press enter:

Open the site actions menu with JavaScript

The menu will now popup. Don't hover over the menu because you trigger the onmouseout event and the menu will disappear after a while.

In Firebug open the HTML tab and search after a text in the menu. (You have to click search a few times to jump to the right code area). The code will jump to the html of generated menu and you can use Firebug to change styles easily.

Searching the right code

And if the site action menu disappears while modifiying it, you can call the script to open it again !

Ugly menu example