Wednesday, January 31, 2007
by Nik Kalyani
Wednesday, January 31, 2007 10:54:34 AM (Pacific Standard Time, UTC-08:00)

In a previous post DotNetNuke Module Settings Made Simple, I had shared some code to make DotNetNuke module settings easier to manage. One of the methods in an included class permitted the use of tokens in stored settings values. This allows for many possibilities that are user-, tab- or portal-driven. In that post, I had omitted the code for token substitution because it is a bit lengthy. Here, then, is code for substituting string tokens with portal values.

Although the intended use is for module settings, there’s no reason this code cannot be used for just about any scenario where tokens need to be injected. 

using System;
using System.Web;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections;
using System.Reflection;
using DotNetNuke;
using DotNetNuke.Entities.Modules;
using DotNetNuke.Entities.Users;
using DotNetNuke.Entities.Tabs;
using DotNetNuke.Security;
using DotNetNuke.Security.Roles;
using DotNetNuke.Common;
using DotNetNuke.Common.Utilities;
using DotNetNuke.Services.Localization;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Entities.Modules.Actions;
using DotNetNuke.Services.Personalization;
using DotNetNuke.Modules.HTMLEditorProvider;
using DotNetNuke.Framework.Providers;
/* Copyright (c) 2007, Speerio, Inc. This notice may not be removed. */
namespace Speerio.DNN.Common
{
    /// <summary>
    /// Summary description for PortalTokens.
    /// </summary>
    public class PortalTokens
    {
        public const string UnknownUser = "~Public";
 
        public static string Replace(string source)
        {
            return(Replace(source, UnknownUser, null));
        }
 
        public static string Replace(string source, ModuleInfo moduleConfig)
        {
            return(Replace(source, UnknownUser, moduleConfig));
        }
 
        public static string Replace(string source, string unknownUserText, ModuleInfo moduleConfig)
        {
            PortalSettings portalSettings = (PortalSettings) HttpContext.Current.Items["PortalSettings"];
 
            // PORTALSETTINGS tokens
            source = PortalTokens.ReplacePortalTokens(source, portalSettings);
 
            // USERINFO tokens
            source = PortalTokens.ReplaceUserTokens(source, unknownUserText);
 
            // ROLEMEMBER tokens
            source = PortalTokens.ReplaceRoleTokens(source);
 
            // TABSETTINGS tokens
            source = PortalTokens.ReplaceTabTokens(source, portalSettings.ActiveTab);
 
            // MODULESETTINGS tokens
            if (moduleConfig != null)
                source = PortalTokens.ReplaceModuleTokens(source, moduleConfig);
 
            // USERPROFILE tokens
            source = PortalTokens.ReplaceProfileTokens(source);
 
            // REGEX tokens
            source = PortalTokens.ReplaceRegExTokens(source);
 
            return(source);
        }
 
        private static string ReplaceRegExTokens(string source)
        {
            // REGEX tokens
            if (source.IndexOf("<REGEX:") > -1)
            {
                int reToken = source.IndexOf ("<");
                if (reToken > -1)
                {
                    try
                    {
                        string rePair = source.Substring(reToken+1);
                        int term = rePair.IndexOf(">");
                        if (term > -1)
                        {
                            rePair = rePair.Substring(0,term);
                            string[] reParams = rePair.Split(',');
                            if (reParams.Length == 2)
                            {
                                source = source.Replace("<" + reParams[0] + "," + reParams[1] + ">","");
                                reParams[0] = reParams[0].Replace("REGEX:","");
                                source = Regex.Replace(source, reParams[0], reParams[1], RegexOptions.IgnoreCase);
                            }
                        }
                    }
                    catch
                    {
                    }
                }
            }
            return(source);
        }
                        
        private static string ReplacePortalTokens(string source, PortalSettings portalSettings)
        {
            if (source.IndexOf("[PORTALSETTINGS:") > -1)
            {
                try { source = source.Replace("[PORTALSETTINGS:PortalId]",portalSettings.PortalId.ToString()); } catch {}
                try { source = source.Replace("[PORTALSETTINGS:UploadDirectory]",portalSettings.HomeDirectory); } catch {}
                try { source = source.Replace("[PORTALSETTINGS:HomeDirectory]",portalSettings.HomeDirectory); } catch {}
                try { source = source.Replace("[PORTALSETTINGS:PortalName]",portalSettings.PortalName); } catch {}
                try { source = source.Replace("[PORTALSETTINGS:AdministratorId]",portalSettings.AdministratorId.ToString()); } catch {}
                try { source = source.Replace("[PORTALSETTINGS:AdministratorRoleId]",portalSettings.AdministratorRoleId.ToString()); } catch {}
                try { source = source.Replace("[PORTALSETTINGS:DefaultLanguage]",portalSettings.DefaultLanguage); } catch {}
            }
            return(source);
        }
 
        private static string ReplaceUserTokens(string source, string unknownUserText)
        {
            if (source.IndexOf("[USERINFO:") > -1)                        
            {
                try
                {
                    bool auth = HttpContext.Current.Request.IsAuthenticated;
                    UserInfo userInfo = null;
                    if (auth)
                        userInfo = (UserInfo) HttpContext.Current.Items["UserInfo"];
 
                    try { source = source.Replace("[USERINFO:UserID]",(auth ? userInfo.UserID.ToString() : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:FullName]",(auth ? userInfo.FirstName + " " + userInfo.LastName : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:FirstName]",(auth ? userInfo.FirstName : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:LastName]",(auth ? userInfo.LastName : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Street]",(auth ? userInfo.Profile.Street : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:City]",(auth ? userInfo.Profile.City : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Region]",(auth ? userInfo.Profile.Region : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:PostalCode]",(auth ? userInfo.Profile.PostalCode : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Country]",(auth ? userInfo.Profile.Country : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Password]",(auth ? userInfo.Membership.Password : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Email]",(auth ? userInfo.Membership.Email : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Unit]",(auth ? userInfo.Profile.Unit : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Telephone]",(auth ? userInfo.Profile.Telephone : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Username]",(auth ? userInfo.Username : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:IsSuperUser]",(auth ? userInfo.IsSuperUser.ToString() : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:AffiliateID]",(auth ? userInfo.AffiliateID.ToString() : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Website]",(auth ? userInfo.Profile.Website : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:TimeZone]",(auth ? userInfo.Profile.TimeZone.ToString() : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:IM]",(auth ? userInfo.Profile.IM : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Fax]",(auth ? userInfo.Profile.Fax : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Cell]",(auth ? userInfo.Profile.Cell : unknownUserText)); } catch {}
 
                }
                catch
                {
                }
            }
            return(source);
        }
 
 
        private static string ReplaceRoleTokens(string source)
        {
            string tmp = source;
 
            if (source.IndexOf("<ROLEMEMBER:") > -1)
            {
                try
                {
                    string pattern = "(?<token>\\<ROLEMEMBER:(?<roles>.*?)\\|(?<unknown>.*?)\\>)";
                    Regex r = new Regex(pattern, RegexOptions.IgnoreCase );
 
                    ArrayList tokens = new ArrayList(10);
                    ArrayList roles = new ArrayList(10);
                    ArrayList unknowns = new ArrayList(10);
                    Match m;                           
                    for (m = r.Match(source); m.Success; m = m.NextMatch())
                    {
                        tokens.Add(m.Groups["token"].ToString());
                        roles.Add(m.Groups["roles"].ToString().Trim());
                        unknowns.Add(m.Groups["unknown"].ToString().Trim());
                    }
 
                    RoleController roleController = new RoleController();
                    bool auth = HttpContext.Current.Request.IsAuthenticated;
                    UserInfo userInfo = null;
                    string[] userRoles = {};
                    if (auth)
                    {
                        userInfo = (UserInfo) HttpContext.Current.Items["UserInfo"];
                        userRoles = roleController.GetPortalRolesByUser(userInfo.UserID, userInfo.PortalID);
                        if (userRoles != null)
                        {
                            for(int u=0;u<userRoles.Length;u++)
                                userRoles[u] = userRoles[u].ToLower();
                        }
                    }
                    for(int idx=0;idx<tokens.Count;idx++)
                    {
                        string token = tokens[idx].ToString();
                        string defaultValue = unknowns[idx].ToString();
                        string[] roleList = roles[idx].ToString().Split(',');
                        string selectedRole = "";
                        for(int l=0;l<roleList.Length;l++)
                        {
                            for(int u=0;u<userRoles.Length;u++)
                            {
                                if (roleList[l].ToLower() == userRoles[u])
                                {
                                    selectedRole = roleList[l];
                                    break;
                                }
                            }
                            if (selectedRole != "")
                                break;
                        }
 
                        try { source = source.Replace(tokens[idx].ToString(), (selectedRole == "" ? defaultValue : selectedRole));  } 
                        catch {}
                    }
                    return(source);
                }
                catch
                {              
                }
            }
            return(source);
        }
 
 
        private static string ReplaceProfileTokens(string source)
        {
            string tmp = source;
 
            if (source.IndexOf("<USERPROFILE:") > -1)
            {
                try
                {
                    string pattern = "(?<token><USERPROFILE:(?<container>.*?),(?<key>.*?),(?<unknown>.*?)>)";
                    Regex r = new Regex(pattern, RegexOptions.IgnoreCase );
 
                    ArrayList tokens = new ArrayList(10);
                    ArrayList containers = new ArrayList(10);
                    ArrayList keys = new ArrayList(10);
                    ArrayList unknowns = new ArrayList(10);
                    Match m;                           
                    for (m = r.Match(source); m.Success; m = m.NextMatch())
                    {
                        tokens.Add(m.Groups["token"].ToString());
                        containers.Add(m.Groups["container"].ToString().Trim());
                        keys.Add(m.Groups["key"].ToString().Trim());
                        unknowns.Add(m.Groups["unknown"].ToString().Trim());
                    }
                    for(int idx=0;idx<tokens.Count;idx++)
                    {
                        object keyObject = Personalization.GetProfile(containers[idx].ToString(),keys[idx].ToString());
                        string keyValue = "";
                        if (keyObject != null)
                            keyValue = keyObject.ToString();
                        try { source = source.Replace(tokens[idx].ToString(), (keyValue == "" ? unknowns[idx].ToString() : keyValue));  } catch {}
                    }
                    return(source);
                }
                catch
                {              
                }
            }
            return(source);
        }
 
        private static string ReplaceTabTokens(string source, TabInfo tabSettings)
        {
            if (source.IndexOf("[TABSETTINGS:") > -1)
            {
                try
                {
                    try { source = source.Replace("[TABSETTINGS:PortalId]",tabSettings.PortalID.ToString()); } catch {}
                    try { source = source.Replace("[TABSETTINGS:TabId]",tabSettings.TabID.ToString()); } catch {}
                    try { source = source.Replace("[TABSETTINGS:TabName]",tabSettings.TabName); } catch {}
                    try { source = source.Replace("[TABSETTINGS:Title]",tabSettings.Title); } catch {}
                    try { source = source.Replace("[TABSETTINGS:AuthorizedRoles]",tabSettings.AuthorizedRoles); } catch {}
                    try { source = source.Replace("[TABSETTINGS:AdministratorRoles]",tabSettings.AdministratorRoles); } catch {}
                    try { source = source.Replace("[TABSETTINGS:ParentId]",tabSettings.ParentId.ToString()); } catch {}
                    try { source = source.Replace("[TABSETTINGS:Level]",tabSettings.Level.ToString()); } catch {}
                    try { source = source.Replace("[TABSETTINGS:Skinsource]",tabSettings.SkinPath); } catch {}
                    try { source = source.Replace("[TABSETTINGS:SkinSrc]",tabSettings.SkinSrc); } catch {}
                }
                catch
                {
                }
            }
            return(source);
        }
 
        private static string ReplaceModuleTokens(string source, ModuleInfo moduleConfig)
        {
            if (source.IndexOf("[MODULESETTINGS:") > -1)
            {
                try
                {
                    try { source = source.Replace("[MODULESETTINGS:ModuleId]",moduleConfig.ModuleID.ToString()); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:TabId]",moduleConfig.TabID.ToString()); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:ModuleDefId]",moduleConfig.ModuleDefID.ToString()); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:ModuleOrder]",moduleConfig.ModuleOrder.ToString()); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:PaneName]",moduleConfig.PaneName); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:ModuleTitle]",moduleConfig.ModuleTitle); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:AuthorizedEditRoles]",moduleConfig.AuthorizedEditRoles); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:AuthorizedViewRoles]",moduleConfig.AuthorizedViewRoles); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:ControlSrc]",moduleConfig.ControlSrc); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:ControlTitle]",moduleConfig.ControlTitle); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:DesktopModuleId]",moduleConfig.DesktopModuleID.ToString()); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:FriendlyName]",moduleConfig.FriendlyName); } catch {}
 
                }
                catch
                {
                }
            }
            return(source);
        }
 
    }
}
#    Comments [1] - Trackback    

 Tuesday, January 30, 2007
by Nik Kalyani
Tuesday, January 30, 2007 6:50:14 PM (Pacific Standard Time, UTC-08:00)

Many businesses that develop websites for customers using DotNetNuke have to field questions about reference sites. Here is the answer —


Computer Hardware:

Manufacturing:

Software:

Pharma:

Travel/Logistics:

Education:

Financial Services:

Non-Profits:

Entertainment:

 

#    Comments [1] - Trackback    

 Monday, January 29, 2007
by Nik Kalyani
Monday, January 29, 2007 10:23:52 AM (Pacific Standard Time, UTC-08:00)

I naively assumed that the Office team dog-fooded the Outlook 2007 RSS Feeds capability before shipping the product. Based on my experience, I am inclined to believe that no testing was done on this aspect of Outlook. Here are some issues that are obvious bugs or shortcomings. Some of these are so blatant that it makes you wonder if anyone on the Office team even uses RSS on an everyday basis.

RSS Page: There is no provision (at least none I could find) for easily customizing the page you see when you click RSS Feeds in Outlook. What I expect to see is a summary of the newest items from all the feeds in my subscription list. Instead I see a useless intro to RSS and a more useless directory of various Microsoft feeds.

Adding Feeds: Adding feeds should be as simple and easy as possible. If I chance upon a good feed in my browser, I am inclined to right-click and copy the feed link. I then right-click on RSS Feeds and select “Add a new RSS feed…” which displays the “New RSS Feed” dialog. This is where the problem begins. I cannot right-click and paste the link in my clipboard. I have no idea why. Instead, I have to use Ctrl-V or Edit – Paste. This is a nuisance. I also have no ability to customize anything about the feed when I add it. To do that, I have to click “Tools,” “Account Settings…,” “RSS Feeds,” highlight feed, “Change…”… This is ridiculously complicated.

Feed Handling: This is where things go seriously wrong. I am convinced that Outlook 2007 has a built-in mechanism for arbitratily flagging feed items as read or unread with the additional capability of automatically duplicating items. This gets annoying very quickly, especially on blog feeds. It’s a simple XML feed and Outlook should be able to figure out if an item is already present and not duplicate it. The read/unread flagging is also not rocket science.

Post Display: Reading a post with images is another annoyance. Most post images are not important, but when a blogger has posted a screen-cap or a photo, one needs to be able to look at the image. Outlook will mysteriously hide the images in a post until you click on it. This is weird behavior. Once you select the option to be able to view images, you should be able to view the images. Requiring an additional click on each image cannot be an intentional feature…this has to be a bug and a pretty serious one too.

All-in-all the RSS reader in Outlook is junk. It’s one saving grace is the ability to export the list of feeds in OPML format so you can switch to different reader.

 

#    Comments [2] - Trackback    

by Nik Kalyani
Monday, January 29, 2007 9:19:07 AM (Pacific Standard Time, UTC-08:00)

Steve Krug loves tabs for web page navigation and shares examples of tabs that work and those that don’t in his popular book on web usability “Dont’ Make Me Think.” In reading his thoughts on tabs, one insight that I gained is that for tabs to work really well, the visual representation should mimic real-world tabs used in binders, reports etc. While there are many scripts and components available for tabs, most of them tend to ignore an important aspect of tabs — overlapping. Some developers/designers use faux 3D to give this appearance, but nothing beats the real thing. Some quick CSS and Javascript later, I came up with the following:

OverlappingTabs

The overlap is subtle, but in my testing, it does make a difference as the tab content pops up versus just appearing. You can see the difference it makes if you take a look at a tabbed-dialog in Windows. The overlap of tabs is minimal, but visually, it makes quite a difference.

My solution was to create a tab script that works equally well whether the tabs are created directly in HTML or indirectly through server-side script. The HTML for creating the above is quite simple:

<div class="tabContainer">
 <ul id="MyTabs"><li>Home</li><li>Products</li><li>Support</li><li>About</li></ul>
</div>

To “tabify” this code, you use the following Javascript:

renderTabs(“MyTabs”);

To add more client interactivity, you can pass two additional (optional) parameters:

– name of a function to call when a tab is clicked. The function is passed the unordered list DOM object and tab index that was clicked as parameters
– index of tab to display

Here’s an example:

renderTabs(“MyTabs”, “clickHandler”);

function clickHandler(ul, tabIndex)
{
      alert(“Tab “ + tabIndex + “ was clicked.”);
}

The script, including CSS, images and a sample handler is here: OverlappingTabs.zip (tested with IE6/7 and Firefox 1.5)

 

#    Comments [1] - Trackback    

 Wednesday, January 24, 2007
by Nik Kalyani
Wednesday, January 24, 2007 10:04:34 PM (Pacific Standard Time, UTC-08:00)

I needed a way to present the user with a hierarchical list of categories in an ASP.Net application. I also needed the code to be light-weight and the UI to be simple and customizable with CSS. Having AJAX functionality was not important as the number of categories in the list will not be very large. After checking-out several commercial tree components, existing JS libraries and various code snippets, I decided to write my own as I did not find anything that matched my needs.

The resulting “checkboxList” class is simple and easy to use. I have completed the Javascript coding, but still need to wire it up into an ASP.Net component. Below is the working Javascript code with some usage notes.

Usage:

checkboxList(instanceVariableName, trackingFieldId, containerId)

instanceVariableName: Name of variable with a reference to the object
trackingFieldId: The client-side ID of a text field where a list of checked item IDs will be stored delimited by “|”
containerId: The client-side ID of an HTML container element (such as DIV) where the list will be injected

Example: var myList = new checkboxList(“myList”, “trackingField”, “container”);

Adding items to the checkbox list is done using the appropriately named “add()” method as follows:

checkboxList.add(itemId, itemLabel, isChecked[, parentItemId])

Examples:
myList.add(“node1”, “My First Node”, false);
myList.add(“node2”, “My Second Node”, false);
myList.add(“node3”, “Child of First Node”, true, “node1”);

After adding nodes as desired, call the “render()” method to render the checkbox list to the browser:

myList.render();

One important thing to remember is that the “render()” method expects to already find the container element. If the browser has not yet created the container element, then the checkbox list will not be rendered. The result will look something like this:

Cblist

The text field where the values will be stored should ideally be hidden (displayed here just to show the data values). For simplicity, I decided not to include images and instead rely on CSS styling to indicate if a node has children and is expandable. There are three style classes:

node: appearance of the text label
nodeChild: appearance of the SPAN element used for a child node block
nodeParent: appended to “node” for nodes that contain child nodes

Here’s the code:

 
<style>
body
{
    font: 10pt Verdana,sans-serif;
    color: navy;
}
.node
{
    cursor: pointer;
    cursor: hand;
    display: block;
}
 
.nodeChild
{
    display: none;
    margin-left: 16px;
}
 
.nodeParent
{
    font-weight: bold;
}
 
.scrollingList
{
    height: 120px;
    width: 250px;
    overflow: auto;
    border:1px solid #e0e0e0;
}
</style>
 
<script language="JavaScript">
 
function checkboxList(instanceName, trackingElementId, containerElementId)
{
    this.instanceName = instanceName;
    this.add = addNode;
    this.render = renderCheckboxList;
    this.allNodes = new Object; 
    this.tracker = document.getElementById(trackingElementId);
    this.containerElementId = containerElementId;
}
 
function node(id, text, checked)
{
    this.id = id;
    this.text = text;
        this.checked = checked;
    this.parentId = null;
    this.show = expandParent;
    this.rootInstance = '';
}
 
function expandParent()
{
    // Expands the parent node causing a node to be displayed
    // This is automatically done when rendering to ensure
    // that all checked nodes are visible
 
    var p = this.parent;
    while(p)
    {
        var el = document.getElementById(p.id);
        el.style.display = 'block';
        p = p.parent;        
    }
}
 
 
function renderCheckboxList()
{
    // Renders all checkboxes
    var checkboxListString = '';
    for(var n in this.allNodes)
    {
        if (!this.allNodes[n].parentId)
            checkboxListString += this.allNodes[n].render(this);
    }
 
    var container = document.getElementById(this.containerElementId);
 
    if (container)
        container.innerHTML = checkboxListString;
 
    this.updateValue(true);
 
}
 
checkboxList.prototype.updateValue = function(display)
{
    // "display" controls if the UI is rendered to ensure that every checked node is
    // visible. If "display" is true, a node's show() method is called.
 
        var checkedString = "";
    for(var n in this.allNodes)
    {
        if (this.allNodes[n].checked)
        {
            if (display)
                this.allNodes[n].show();
            checkedString += (checkedString == "" ? "" : "|") + this.allNodes[n].id;
        }
    }
    this.tracker.value = checkedString;    
}
 
checkboxList.prototype.toggle = function(nodeId, checked)
{
    // If the user clicks a checkbox, the corresponding object in the associative array is updated
 
    for(var n in this.allNodes)
    {
        if (this.allNodes[n].id == nodeId)
        {
            this.allNodes[n].checked = checked;
            break;
        }    
    }
 
    // The tracking field is updated, but the nodes are not auto-expanded
    this.updateValue(false);
 
}
 
 
function addNode(id, label, checked, parentId)
{
    var n = new node(id, label, checked);
 
    // If the specified parentId node is not present, the node is
    // added to the top level
    if (this.allNodes[parentId])
        n.parentId = parentId;
    else
        n.parentId = null;
    n.rootInstance = this.instanceName;
    this.allNodes[n.id] = n;
}
 
node.prototype.render = function(root)
{
    // Renders a node to the browser
 
    
    // Obtain a list of child nodes by looking for nodes which have
    // this node's Id as their parentId
    var childNodes = new Array();
    for(var n in root.allNodes)
    {
        if (root.allNodes[n].parentId == this.id)
            childNodes[childNodes.length] = root.allNodes[n].id;
    }
 
    var numNodes = childNodes.length;
 
    // Compose the HTML string for rendering this node
    // Toggling the checkbox calls the toggle method of the root list object
 
    var nodeString = '<span class="node' + (numNodes > 0 ? ' nodeParent' : '') + '">';
    nodeString += '<input type="checkbox" id="cb' + this.id + '" ';
    nodeString += 'onClick="' + this.rootInstance + '.toggle(\'' + this.id + '\', this.checked)"';
    nodeString += (this.checked ? ' checked' : '') + '> ';
    nodeString += '<span' + (numNodes > 0 ? ' onClick="showNode(\'' + this.id + '\')"' : '') + '>' + this.text + '</span>';
    nodeString += '</span>';
 
    // If any child nodes are present, recursively render them also
    if (numNodes > 0)
    {
        nodeString += '<span class="nodeChild" id="' + this.id + '">';
        for(var j=0;j<numNodes;j++)
            nodeString += root.allNodes[childNodes[j]].render(root);
        nodeString += '</span>';
    }
    
    return nodeString;
}
 
 
function showNode(node)
{
    // When a user clicks on a node label
    // this function is called to toggle the display
 
    var objNode = document.getElementById(node).style;
    if(objNode.display=="block")
        objNode.display="none";
    else
        objNode.display="block";
}
 
 
var myCheckboxList = new checkboxList("myCheckboxList","trackingField", "scrollingList");
 
myCheckboxList.add('node1','Node 1', false);
myCheckboxList.add('node2', 'Node 2', false, 'node1');
myCheckboxList.add('node3', 'Node 3', true, 'node2');
myCheckboxList.add('node4', 'Node 4', false, 'node2');
myCheckboxList.add('node5', 'Node 5', false);
myCheckboxList.add('node6', 'Node 6', true, 'node5');
 
</script>
</head>
<body>
<p>
<input type="text" id="trackingField" style="width:400px">
</p>
 
<div id="scrollingList" class="scrollingList" />
<script>
myCheckboxList.render();
</script>

#    Comments [1] - Trackback    

 Tuesday, January 16, 2007
by Nik Kalyani
Tuesday, January 16, 2007 7:34:05 AM (Pacific Standard Time, UTC-08:00)

If you spend any time on people’s blog sites, you have no doubt encountered the “Snap Preview” dialogs that appear when you hover over a link. If you have not, then visit the Snap site and check it out.

Snap

Since these preview dialogs started showing up on blogs, they have generated quite a bit of noise. Many people love them, and many people hate them. Fred Wilson put them on his blog and got a lot of feedback, mostly negative.

Snap seems oblivious to the criticisms. As evident from the blurb on Snap’s site, they are milking the numbers as much as possible. Who wouldn’t want to highlight “65 million+ previews viewed” (which, incidentally, does not take into account that a “preview” happens when your mouse happens to hover or cross over a hyperlink, whether you wanted to see the preview or not)?

My personal opinion of the Snap Preview concept is that it is quite easily one of the stupidest things I have seen on the web. Seriously, what possible benefit could one derive from an annoying popup DIV with thumbnail images and greeked text. What meaningful thing can the Snap Preview do for me that a simple right-click, Open in New Tab or Window can’t do? Oh right, annoy me incessantly and ruin my browsing experience.

Now, the point of this blog post is not to gripe about Snap. I view it as an interesting opportunity to analyze usability gone wrong and provide some suggestions on fixing it. Possibly, quite possibly, with some usability improvements, the Snap Preview might be tolerable and maybe even useful. Here are three tips for improvement:

1) Turn off the display when casually hovering over links. This behavior is totally unacceptable and is no less annoying than ad popups. Instead, modify the script so the preview is displayed only on deliberate hover actions. I consider a deliberate hover one that lasts more than a couple of seconds minimum.

2) Visual Distinction. Although the Snap-enabled links are visually distinctive, it’s too much to expect a visitor coming to your site for the first time to be able to differentiate whether you just happen to style links a certain way or if they are Snap-enabled. I would prefer to see some combination of #1 and #2…perhaps a distinct, branded icon with a reasonable hover delay. (The little word balloon does not qualify as distinct or branded.) 

3) Provide useful information. The preview is cute, but useless. I would much rather get some useful information about the page. What is its Page Rank? What are the key concepts expressed in the page content — show me a quick synopsis or even just display the metadata for the page? Finally — a good use for a tag cloud — analyze the page words on-the-fly and create a tag cloud in the preview. Show me the Digg, Reddit etc. numbers for the page so I can determine if it’s important to other people.

What do you think of Snap? Of these suggestions for usability improvements?

#    Comments [2] - Trackback    

 Monday, January 15, 2007
by Nik Kalyani
Monday, January 15, 2007 8:51:57 PM (Pacific Standard Time, UTC-08:00)

I have been using Windows Media Center Edition for over a year now. When Vista Ultimate became available, I immediately upgraded because I was drawn to the improvements in the UI. Now, several months later, I am not so sure. Although it’s unlikely that I will revert to XP Media Center, I will have to live with the following things that I don’t like about Vista Media Center. To be honest, not all of these problems are new…some of them plagued XP Media Center too. However, since this is a “Why I Don’t Like…” list, I’ll lump them all together.

1) Frequent lock-ups

My hardware hasn’t changed. It was stable with XP Media Center and my reboots were limited to manual ones when updating software etc. These were few and far-between. Now, with Vista Media Center, it seems like I have to reboot daily, sometimes multiple times. The 10–ft experience has become a 1–ft experience and is trying my patience. This is with a clean install of Vista. I tried the 64–bit version and am now on the 32–bit version and have not seen any improvement. Most lock-ups occur while viewing recorded TV. Although there is no good time for the computer to lock-up, this has to be the worst time. Vista was supposed to be more stable…what happened?

2) Media Folders

I am extremely picky about how I organize my data, especially media. I have photos organized by year, with event sub-folders and the same with videos. I recently consolidated the two to accommodate Media Center.

Unfortunately though, Media Center inherits some settings and the “media discovery” approach from Windows Media Player. Now, Windows Media Player continues to be the suckiest media player ever. I have successfully avoided using it for many years, but with Media Center I have no choice. When I tell Media Center where to look for media, it does it in the stupidest way possible. It displays folders in the UI even if they have the Hidden attribute set and even if they are EMPTY. What idiot developer wrote this portion of Media Center? If I navigate to “Picture Library” I will see many folders which happen to only contain videos and therefore show up empty. If I navigate to “Video Library” the opposite happens. A simple test to see if any media of the type being displayed exists before displaying a folder choice seems like the logical thing here. But that is not the case. This is the developer mind-set prevailing over the usability mind-set. It’s more efficient to not enumerate the contents of the folder, so let’s just display the folder even if it’s empty.

Wait, there’s more. When I add additional storage folders for Recorded TV, Media Center decides to use these folders for my Photos/Videos section too. Why? It makes no sense? There is a dedicated selection called Recorded TV. This is the only place I expect to find Recorded TV.

3) Music

Media Center’s ability to select music is nice. There are options to view by album, artist, genre etc. But it’s no good for me as I don’t want Media Center to decide how my music is organized…I want to decide how my music is organized. I have over 50 Gb of ripped music in English, Hindi and Gujarati. Since AMG is unable to help Media Center (or WMP) identify most of my music, the net result is that I have a selection called “Various” that has some 8,000 tracks. What a mess!

My music is already neatly organized by language, then genre and then album. All I want is to be able to navigate this hierarchy. This is virtually impossible to do with Media Center.

4) Movies

This is where Media Center almost got it right. The default “Play DVD” option is lame. But once you make the well-documented registry hack and enable “DVD Library” you unlock a much nicer movie-watching experience. I have two Terastations with a combined 3TB of ripped DVD movies. Again, same deal…organized by language (Hollywood, Bollywood etc.), genre and movie. This is based on my family’s movie watching preference. Shall we watch an English or Hindi movie? Action, Comedy or Romance? Easy.

Since DVD library relies on the incomplete AMG meta data, this too is not possible and the end-result is that I have to page through hundreds of movies to find one I want to watch. It’s all hit and miss.

I tried “My Movies” and found that to be even worse. It insists on putting a large disc icon overlay on all the movie covers which is entirely stupid since it conveys no useful information.

All told, I am fed-up with Media Center shortcomings. There are several other annoyances, but the above four are the main reasons I don’t like Media Center. I am not sure what I can do about the lock-ups other than try different hardware, which I will in a month or so. But I am frustrated enough with the remaining three issues, that I am going to do something about it.

My Solution

The sample “Z” application that comes with the Media Center SDK has a really nice UI. My idea is to hack it and create a “folder-based” media experience so that Media Center will respect how I have organized my media versus forcing its own convoluted approach on me.

My other idea is to remove the management aspects of the media from the Media Center computer. Whether you manage the media, cover art, meta data etc. using the remote or at the Media Center PC itself, both approaches are terrible from a usability and convenience standpoint. What I would like to do is create an embedded web server for Media Center and add-on DotNetNuke. Add a few modules and Voila! I have the perfect system. Not only will I be able to manage my media from any web browser, but I will also be able to selectively share photos and videos with others without having to replicate them from my home storage to some other photo/video sharing site such as Pickle.

These solutions require a fair amount of work and I am not sure how much time I will have in coming months, but I love working with media and I love creating usable apps, so this will be a good break from more serious work-related development.

 

#    Comments [2] - Trackback    

by Nik Kalyani
Monday, January 15, 2007 8:05:37 PM (Pacific Standard Time, UTC-08:00)

One of my dedicated servers at PowerDNN is running 64–bit Windows 2003 Server. Recently I had to get an app that was ASP.Net 1.1–dependent to run on this box. No problem, right? Wrong…big problem. Turns out that in order to run ASP.Net 1.1 and 2.0 concurrently on 64–bit Windows Server, you have to run IIS in 32–bit mode. Here’s the reason according to Microsoft KB894435:

IIS 6.0 supports both the 32-bit mode and the 64-bit mode. However IIS 6.0 does not support running both modes at the same time on a 64-bit version of Windows. ASP.NET 1.1 runs only in 32-bit mode. ASP.NET 2.0 runs in 32-bit mode or in 64-bit mode. Therefore, if you want to run ASP.NET 1.1 and ASP.NET 2.0 at the same time, you must run IIS in 32-bit mode.

Unfortunate, but understandable. I followed the instructions in the article and everything worked fine until I needed to switch the ASP.Net version for the app that I mentioned earlier. Despite having both versions of ASP.Net installed, the ASP.Net tab was not displayed.

IIS Properties Dialog

Googling a bit, I discovered that this problem often appeared when upgrading from an ASP.Net 2 beta to the production version. The fix was to delete some obsolete registry keys and register the framework again using aspnet_regiis. I did this, rebooted and saw no difference.

After spending almost two hours looking for a solution, I decided that there was no easy fix (of course, I was completely wrong). It then occurred to me that since the properties dialog is just a UI for WMI, it should be possible to accomplish the ASP.Net version switching using script. Googled some more and discovered that this was a solution, but not the simplest one. If you know the website ID, then aspnet_regiis can set the ASP.Net version for an IIS site. And Denis Bauer (of Reflector File Disassembler fame) had already solved the problem with a neat utility called ASP.Net Version Switcher.

ASPNETVersionSwitcher

This utility solved the problem and had me up and running in seconds. I selected the site, clicked the ASP.Net version I wanted and hit the Switch button. Done.

Thanks, Denis.

#    Comments [0] - Trackback    

 Tuesday, January 09, 2007
by Nik Kalyani
Tuesday, January 09, 2007 12:48:49 PM (Pacific Standard Time, UTC-08:00)

Unbelievable. Apple has done it again with the most amazing iPhone. This phone is a minimalist person’s dream. Looks like I am going to be switching from T-Mobile to Cingular this summer so I can get my hands on this beauty.

The features I find most compelling are:

  • Only one button and no keypad
  • Full fidelity web browsing
  • Intelligent, non-sequential voice mail
  • Portrait or landscape mode

My current phone is a Sony-Ericsson W800i. It has a number of the iPhone features, packaged in a very usable Sony aesthetic package. However, Apple’s minimalist approach to the hardware and the tightly integrated software puts it in a whole different league. 

IPhone

#    Comments [0] - Trackback    

RSS feed
Search and Links
Bling

View Nik Kalyani's profile on LinkedIn

TechBubble
www.flickr.com
This is a Flickr badge showing public photos from techbubble. Make your own badge here.
Statistics
Total Posts: 216
This Year: 19
This Month: 0
This Week: 0
Comments: 226
About the author/Disclaimer

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2008
Nik Kalyani
Sign In
All Content © 2008, Nik Kalyani