June 2005 - Posts
Have a look at this HTML snippet. When clicking the 'clickme' innerText of div2, I expect the browser to alert "div2", because div2 is the first child of its parent, div1.
<
div id="div1">
<div id="div2" onclick="alert(this.parentNode.childNodes[0].id)">clickme</div>
</div> I've tested this HTML snippet on both IE and FireFox, and guess what?
IE reacts as expected by alerting "div2".
However, FireFox alerts "undefined"....?
Apparently, in FireFox div2 is considered to be the second childNode of div1; not the first. I guess this is because FireFox parses the whitespace between div1 and div2 as being a seperate node! BUT WHY????
Did the creators of FireFox (Mozilla Foundation) implemented this by design? Is it ever going to be fixed in FireFox? Or do we just have to live with it, and write seperate javascript for IE and FireFox?
[Note: of course I can let IE and FF return the same results by removing all whitespace between the div1 and div2 tags, but that would result in human-unreadable and hard to maintain Html.]
Postscript:
I've received a lot of comments about the fact that FireFox precisely (I'd say 'rigidly') follows the w3c DOM recommendation on this. I'm aware of the w3c specs, but I still think it sucks having to write seperate code for IE and FF (or include a library of wrapper functions).
Personally, I think the IE dom-representation is the more sensible one, whether it's in the recommendation or not.
And FireFox just made a very bad 'first impression' to me :(
Read these links on more information why Mozilla has chosen to preserve whitespaces in the DOM:
If you've been experimenting with the ASP.NET 2.0 Portal Framework (like me!), you know there are 4 out-of-the-box EditorParts that you can place in the EditorZone (the EditorZone is the Zone where you can edit a WebPart's properties). The 4 standard EditorParts are:
- PropertyGridEditorPart
- AppearanceEditorPart
- BehaviorEditorPart
- LayoutEditorPart
The Problem
These EditorParts work great. However, if you place all 4 EditorParts in the EditorZone, they will show a very long list of editable properties. Not very user-friendly.
The Solution
By using stylesheet classes and a little client-side javascript, you can make the EditorParts expand and collapse when you click on their titles. You can even show a cool plus/minus icon next to the EditorPart's title (see screenshot).

How it's done: The Code
In the EditorZone tag, set the CssClass-attribute of the EditorParts you want default to be collapsed to 'EditorPartHidden' (this is a css class I'll show you later on). Also set the CssClass of the PartTitleStyle to 'EditorPartTitle' (so a client-side behavior can set on the EditorPart's title).
<
asp:EditorZone runat="server" ID="EditorZone1">
<ZoneTemplate>
<asp:AppearanceEditorPart CssClass="EditorPartHidden" ID="epart1" runat="server" />
<asp:BehaviorEditorPart CssClass="EditorPartHidden" ID="epart2" runat="server" />
<asp:LayoutEditorPart CssClass="EditorPartHidden" ID="epart3" runat="Server" />
</ZoneTemplate>
<PartTitleStyle CssClass="EditorPartTitle" />
</asp:EditorZone> Next you can see the 'EditorPartHidden' and 'EditorPartTitle' css classes (put these in your stylesheet). Note that the 'EditorPartTitle' class is using a background image to show the 'plus' sign next to the title (also needs a padding-left to move the title a bit to the right, otherwise the title will be placed on top of the 'plus' sign).
A behavior (toggle.htc) is set on the EditorPartTitle to perform the expand/collapse. Note: behaviors are IE 5.5+ only (or does FireFox supports this also?).
Note: I replaced the .htc (IE only behavior) by cross-browser javascript.
.EditorPartHidden
{
display: none;
} .EditorPartTitle
{
behavior: url(htc/toggle.htc); //note: I replaced the .htc (IE only) by cross-browser javascript
background-position: left;
background-repeat: no-repeat;
background-image: url(images/plus.gif);
cursor: pointer;
padding-left: 14px;
font-size: x-small;
} ...and this is the javascript code for the expand/collapse behavior. It sets onclick events to all EditorPart titles, which toggles the image (plus/minus) and the EditorPart's content. Include this code on your .aspx page (or use Page.ClientScript.RegisterClientScriptBlock).
<script type="text/javascript">
// Create expanding titles for Legends with className 'EditorPartTitle'
function CreateExpandingTitles() {
var elements = document.getElementsByTagName("LEGEND");
for (i=0; i<elements.length; i++) {
if (elements[i].className && elements[i].className == "EditorPartTitle")
elements[i].onclick = new Function("toggle(this);");
}
} // Call function CreateExpandingTitles on window onload
if (window.addEventListener)
window.addEventListener('load', CreateExpandingTitles, false);
else if (window.attachEvent)
window.attachEvent('onload', CreateExpandingTitles);
function toggle(titleElement) {
// Find nextSibling's firstChild (i.e. DIV with class 'EditorPartStyleHidden')
// For IE this is nextSibling.childNodes[0], but due to an error in current
// version of FireFox (1.0.4) this is nextSibling.childNodes[1]
var firstChild = (titleElement.nextSibling.childNodes[0].id)
? titleElement.nextSibling.childNodes[0]
: titleElement.nextSibling.childNodes[1];
// Toggle image and show/hide EditorPart display
if (firstChild.style.display == "block") {
firstChild.style.display = "none";
titleElement.style.backgroundImage = "url(images/plus.gif)";
} else {
firstChild.style.display = "block";
titleElement.style.backgroundImage = "url(images/minus.gif)";
}
}
</script>
Some developers in the ASP.NET 2.0 Forums have asked questions on how to copy a WebPart (including it's configuration) from one page to another.
The standard solution for this is to use the 'Export/Import' functionality that ASP.NET 2.0 offers for WebParts. Note: for a WebPart to be export-enabled, its 'ExportMode' property should have a value other than 'None' (which is the default) and enableExport="true" should be set in the webParts section of the web.config. The export and import of a (configured) WebPart is done in six steps:
- Switch to EditDisplayMode
- Select 'Export' from the verbs menu of the WebPart you want to copy
- Download the WebPart file to your local PC
- Navigate to the destination page, switch to CatalogDisplayMode
- Select the 'Imported Web Part Catalog', and upload the WebPart file
- Add the uploaded WebPart to a zone
Not very user-friendly, is it? Most users wouldn't even know what to do with the downloaded Webpart file.
However, the ASP.NET 2.0 portal framework is very extensible, so I've build a custom solution where the user can just 'copy' the WebPart to a serverside 'clipboard' (i.e. the Session object), and 'paste' it from a custom Catalog (which I named the 'Copied Web Part Catalog'). So this is done without downloading and uploading WebParts files!
Here's a screenshot of the copy/paste functionality:

So you want to know how this is done? I'll show you the code in 3 parts:
Part 1: Add a custom 'Copy' verb to your WebPart code.
/// <summary>
/// Override the Verbs collection to add the 'copy webpart' verb
/// </summary>
public override WebPartVerbCollection Verbs
{
get
{
if (_verbs == null)
{
WebPartVerb myVerb = new WebPartVerb("copy", OnWebPartCopy);
myVerb.Description = "Copy this WebPart";
myVerb.Text = "Copy";
myVerb.ImageUrl = "~/images/CopyVerb.gif";
_verbs = new WebPartVerbCollection(new WebPartVerb[] { myVerb });
}
return _verbs;
}
} /// <summary>
/// Called when the user selects 'Copy WebPart' from the verbs menu.
/// The selected WebPart is Exported to a xml string, and added to
/// a list in the Session. This list is used by the CopiedCatalogPart
/// to add the copied WebParts to a new page or zone
/// </summary>
public void OnWebPartCopy(object sender, WebPartEventArgs e)
{
//get reference to WebPart to copy
WebPart webPart = e.WebPart;
//temporarily set ExportMode to 'All' to enable WebPart exporting
//this is a kind of hack, but assumed legitimate because the exported WebPart
//stays on the server (in Session)
WebPartExportMode origExportMode = ExportMode;
webPart.ExportMode = WebPartExportMode.All;
//export WebPart to string
StringWriter stringWriter = new StringWriter();
XmlTextWriter xmlwriter = new XmlTextWriter(stringWriter);
WebPartManager.ExportWebPart(webPart, xmlwriter);
string copiedWebPart = stringWriter.ToString();
//reset original ExportMode of WebPart
webPart.ExportMode = origExportMode;
//reset original ExportMode in exported WebPart string
XmlDocument doc = new XmlDocument();
doc.LoadXml(copiedWebPart);
XmlNode exportModeNode = doc.SelectSingleNode("//property[@name='ExportMode']");
if (exportModeNode != null)
{
exportModeNode.InnerText = this.ExportMode.ToString();
copiedWebPart = doc.OuterXml;
}
//get or create list of copied WebParts
List<KeyValuePair<string, string>> copiedWebParts;
if (Context.Session["COPIED_WEBPART_KEY"] != null)
{
copiedWebParts = (List<KeyValuePair<string, string>>)
Context.Session["COPIED_WEBPART_KEY"];
}
else
{
copiedWebParts = new List<KeyValuePair<string, string>>();
}
//insert new copied WebPart as first on list
copiedWebParts.Insert(0, new KeyValuePair<string, string>(
string.Format("{0} [copied {1}]", this.Title, DateTime.Now.ToLongTimeString()),
copiedWebPart));
//add list to Session
Context.Session["COPIED_WEBPART_KEY"] = copiedWebParts;
} Part 2: The CopiedWebPartCatalogPart
using
System;
using System.Collections.Generic;
using System.Web.UI.WebControls.WebParts;
namespace myClassLib.CatalogParts
{
/// <summary>
/// Catalog for reading WebParts copied to the users Session (copy/paste functionality)
/// </summary>
public class CopiedWebPartCatalogPart : CatalogPart
{
/// <summary>
/// Overrides the Title to display "Copied Web Part Catalog" by default
/// </summary>
public override string Title
{
get
{
string title = base.Title;
return string.IsNullOrEmpty(title) ? "Copied Web Part Catalog" : title;
}
set
{
base.Title = value;
}
}
//dictionary to hold the availabe copied webparts
private Dictionary<WebPartDescription, string> _webparts
= new Dictionary<WebPartDescription, string>();
/// <summary>
/// Returns the WebPartDescriptions for the catalog part
/// </summary>
public override WebPartDescriptionCollection GetAvailableWebPartDescriptions()
{
_webparts.Clear();
if (Context.Session["COPIED_WEBPART_SESSION_KEY"] != null)
{
List<KeyValuePair<string, string>> copiedWebParts =
(List<KeyValuePair<string, string>>)Context.Session["COPIED_WEBPART_SESSION_KEY"];
foreach (KeyValuePair<string, string> copiedWebPart in copiedWebParts)
{
WebPartDescription desc = new WebPartDescription(
copiedWebParts.IndexOf(copiedWebPart).ToString(),
copiedWebPart.Key,
null,
null);
_webparts[desc] = copiedWebPart.Value;
}
}
return new WebPartDescriptionCollection(_webparts.Keys);
}
/// <summary>
/// Returns a new instance of the WebPart specified by the description
/// </summary>
public override WebPart GetWebPart(WebPartDescription description)
{
//get a xmltextreader to the exported WebPart
System.Xml.XmlTextReader reader =
new System.Xml.XmlTextReader(new System.IO.StringReader(_webparts[description]));
string errorMessage; //will contain errorMessage on import errors
//return an instance of the requested WebPart
return WebPartManager.ImportWebPart(reader, out errorMessage);
}
}
} Part 3: Add the CopiedWebPartCatalogPart in the CatalogZone of your aspx page
<%@ Register TagPrefix="myCatalogParts" Assembly="myClassLib" Namespace="myClassLib.CatalogParts" %>
<asp:CatalogZone ID="CatalogZone" runat="server" >
<ZoneTemplate>
<asp:PageCatalogPart ID="PagePart" runat="server" />
<asp:ImportCatalogPart Visible="false" ID="ImportPart" runat="Server" />
<myCatalogParts:CopiedWebPartCatalogPart ID="PastePart" runat="server" />
</ZoneTemplate>
</asp:CatalogZone>
The Problem
I think the 'default document' option in IIS is a great way to get clean url's without those .aspx extensions. So instead of calling the url 'http://www.mysite.com/news/default.aspx', you could also call 'http://www.mysite.com/news', which will get you to the same page. This is because IIS finds the default document 'default.aspx' in the news folder.
However, the default SiteMapProvider in ASP.NET 2.0 isn't aware of IIS's default documents, and won't highlight the selected node in a Menu or TreeView contol. Take a look at this xml siteMap file; it will result in links to /default.aspx and /news/default.aspx, but the selected node in my Menu won't be highlighted because the url of the siteMapNode and the request's url don't match:
<?
xml version="1.0" encoding="utf-8"?>
<siteMap>
<siteMapNode title="Home" url="~">
<siteMapNode title="News" url="~/news" />
</siteMapNode>
</siteMap> The Solution
Luckily for me, ASP.NET 2.0 has a provider model for the SiteMaps. This means I've just got to override the CurrentNode property of the default XmlSiteMapProvider and add extra logic for recognizing default documents. Take a look at the code of my FolderAware_XmlSiteMapProvider:
using
System;
using System.Web;
class FolderAware_XmlSiteMapProvider : XmlSiteMapProvider
{
public override SiteMapNode CurrentNode
{
get
{
// first let the base resolve the current node
SiteMapNode currentNode = base.CurrentNode;
if (currentNode == null)
{
// now check if a node exists that's pointing to a folder, and current
// RawUrl maps to the folder's default document (default.aspx)
HttpContext context = HttpContext.Current;
if (context != null)
{
string text = context.Request.RawUrl.ToLowerInvariant();
int index = text.IndexOf("/default.aspx");
if (index != -1)
{
currentNode = FindSiteMapNode(text.Substring(0, index));
if (currentNode != null && !currentNode.IsAccessibleToUser(context))
{
currentNode = null;
}
}
}
}
return currentNode;
}
}
} For your webapplication to use this new FolderAware_XmlSiteMapProvider, you've got to define it in your web.config:
<
siteMap defaultProvider="FolderAware_XmlSiteMapProvider">
<providers>
<clear/>
<add siteMapFile="web.sitemap" name="FolderAware_XmlSiteMapProvider" type="FolderAware_XmlSiteMapProvider" />
</providers>
</siteMap> ...and now the Menu, TreeView and BreadCrumbs contols will highlight the currently selected page even if a request to a webfolder was made!
About impersonating your ASP.NET code
By using basic or integrated IIS security, you can set the security context on whose behalf the code is running to the current user by adding these two lines in your web.config:
<
authentication mode="Windows"/>
<identity impersonate="true"/> This can be usefull when you want to call WebServices inside you code that needs the current user's credentials (note: you'll need Kerberos authentication not to run into the two-hop problem, see http://blogs.msdn.com/mjeelani/archive/2004/12/07/275921.aspx).
The problem: No impersonation inside HttpHandlers
While your pages and controls will now run on behalf of the current user, for some reason this impersonation is not done inside your custom HttpHandler! Although the user is authenticated (HttpContext.Current.User.IsAuthenticated is true), all code is still running as ASPNET or the default AppPool identity (just add a watch on System.Security.Principal.WindowsIdentity.GetCurrent()). So by calling a WebService inside your HttpHandler code using the DefaultCredentials, the wrong credentials will be passed and you'll probably get a 404.
The solution: Do your own impersonation
public
IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
{
...
// Create a WindowsImpersonationContext object by impersonating the Windows identity.
WindowsImpersonationContext impersonationContext = ((WindowsIdentity)context.User.Identity).Impersonate();
try
{
//
// Your impersonated calls to WebServices here
//
}
catch {}
finally
{
if (impersonationContext != null)
{
// always undo impersonation
impersonationContext.Undo();
}
}
... ...so, after you impersonate to the identity inside the current HttpContext, your code will now be running on behalf of the currently logged on user, and you can call WebServices using the current user's credentials.
More Posts