Welcome to MSDN Blogs Sign in | Join | Help

A style that will make the entire cell "clickable" in a Menu

Here's a quick tip for the Menu control.  I'm pretty sure there were some good reasons why this wasn't the default but I just can't think of them at the moment. [EDIT: I just remembered one of them, this won't work perfectly with ItemSpacing...]

If you want to make the entire cell clickable (and not just the text), try using a style "display:block; width:100%".  You'll only want to apply this to the <A> inside the cells. 

Here's an example:

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<
html xmlns="http://www.w3.org/1999/xhtml" >
<
head runat="server">
    <title>Untitled Page</title>
    <style type="text/css">
        .smis a { width:100%; display:block; }
    
</style>
</
head>
<
body>
    <form id="form1" runat="server">
    <div>
        <asp:Menu ID="Menu1" runat="server" BackColor="Green" Width="200px">
            <Items>
                <asp:MenuItem Text="ItemA" Value="A"></asp:MenuItem>
                <asp:MenuItem Text="ItemB" Value="B"></asp:MenuItem>
            </Items>
            <StaticHoverStyle BackColor="#FF8080" />
            <StaticMenuItemStyle CssClass="smis" />
        </asp:Menu>
    
    
</div>
    </form>
</
body>
</
html>
 
(ps. Thanks to Pete for helping me figure this out)
Posted by dannychen | 3 Comments
Filed under:

Making the sample XmlSiteMapProvider updatable with Cache dependencies.

One feature that isn't availible in the source for the XmlSiteMapProvider we released is the ability to update when the web.sitemap file has been updated.  The reason we couldn't provide this is that the mechanism we use inside XmlSiteMapProvider relies on some internal methods which map down to native calls.  Now, this can easily be done with cache dependencies but we chose not to modify the code that way so that users would see how the providers were actually coded and not some version designed to mimic the providers.

However, since this is clearly a nice feature and relatively easy to implement, we elected to blog on it after-the-fact.  Here you go, there's only a few line changes:

First, you need to hook up the handler.  It's actually only a few lines of code.  In the GetConfigDocument() function, there's a few lines like this:

if (!String.IsNullOrEmpty(_filename)) {
    
//_handler = new FileChangeEventHandler(this.OnConfigFileChange);
    //HttpRuntime.FileChangesMonitor.StartMonitoringFile(_filename, _handler);
    ResourceKey = (new FileInfo(_filename)).Name;
}

The new version is: 

if (!String.IsNullOrEmpty(_filename)) {
    
CacheItemRemovedCallback _handler = new CacheItemRemovedCallback(OnConfigFileChange);
    
CacheDependency _dep = new CacheDependency(_filename);
    
HttpContext.Current.Cache.Add(GetHashCode().ToString(), "", _dep,
        
Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration,
        
CacheItemPriority.Normal, _handler);                
    ResourceKey = (
new FileInfo(_filename)).Name;
}

However, the entire function needs to be reorganized slightly to ensure this code always runs.  The normal behavior is to cache the results an exit immediately, in the first version of this post, this behavior caused this code to only get executed the first time.  Here's the new version:

        private XmlDocument GetConfigDocument() {

            if (_document == null)

            {

                if (!_initialized)

                {

                    throw new InvalidOperationException(

                        SR.GetString(SR.XmlSiteMapProvider_Not_Initialized));

                }

 

                // Do the error checking here

                if (_virtualPath == null)

                {

                    throw new ArgumentException(

                        SR.GetString(SR.XmlSiteMapProvider_missing_siteMapFile, _siteMapFileAttribute));

                }

 

                if (!Path.GetExtension(_virtualPath).Equals(_xmlSiteMapFileExtension, StringComparison.OrdinalIgnoreCase))

                {

                    throw new InvalidOperationException(

                        SR.GetString(SR.XmlSiteMapProvider_Invalid_Extension, _virtualPath));

                }

 

                // Ensure the appdomain virtualpath has proper trailing slash

                _normalizedVirtualPath =

                    VirtualPathUtility.Combine(AppDomainAppVirtualPathWithTrailingSlash, _virtualPath);

 

                // Make sure the file exists

                CheckSiteMapFileExists();

 

                _parentSiteMapFileCollection = new StringCollection();

                XmlSiteMapProvider xmlParentProvider = ParentProvider as XmlSiteMapProvider;

                if (xmlParentProvider != null && xmlParentProvider._parentSiteMapFileCollection != null)

                {

                    if (xmlParentProvider._parentSiteMapFileCollection.Contains(_normalizedVirtualPath))

                    {

                        throw new InvalidOperationException(

                            SR.GetString(SR.XmlSiteMapProvider_FileName_already_in_use, _virtualPath));

                    }

 

                    // Copy the sitemapfiles in used from parent provider to current provider.

                    foreach (string filename in xmlParentProvider._parentSiteMapFileCollection)

                    {

                        _parentSiteMapFileCollection.Add(filename);

                    }

                }

 

                // Add current sitemap file to the collection

                _parentSiteMapFileCollection.Add(_normalizedVirtualPath);

 

                _filename = HostingEnvironment.MapPath(_normalizedVirtualPath);

                _document = new ConfigXmlDocument();

            }

 

            if (!String.IsNullOrEmpty(_filename))

            {

                CacheItemRemovedCallback _handler = new CacheItemRemovedCallback(OnConfigFileChange);

                CacheDependency _dep = new CacheDependency(_filename);

                HttpContext.Current.Cache.Add(GetHashCode().ToString(), "", _dep,

                    Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration,

                    CacheItemPriority.Normal, _handler);

                ResourceKey = (new FileInfo(_filename)).Name;

            }

 

            return _document;

        }

 

Next, look for the OnConfigFileChange() handler:

//private void OnConfigFileChange(Object sender, FileChangeEvent e)
//{
//    // Notifiy the parent for the change.
//    XmlSiteMapProvider parentProvider = ParentProvider as XmlSiteMapProvider;
//    if (parentProvider != null)
//    {
//        parentProvider.OnConfigFileChange(sender, e);
//    }
//    Clear();
//}

And change it to this (only the function signature and the recursive call changes):

private void OnConfigFileChange(string key, object value, CacheItemRemovedReason reason)
{
                
    
// Notifiy the parent for the change.
    UpdatableXmlSiteMapProvider parentProvider = ParentProvider as UpdatableXmlSiteMapProvider;
    
if (parentProvider != null)
    {
        parentProvider.OnConfigFileChange(key, value, reason);
    }
    Clear();
}

And that's all there is to it.  The provider will now update with file changes (or any other time the key is forced out of cache).

Posted by dannychen | 0 Comments

Source to the StaticSiteMapProvider and XmlSiteMapProvider (and others) availible for download.

I might be more excited about this than I was about the Web Application Projects preview we just released...

One of the last things I worked on in the ASP.NET runtime test team was validating a version of the sources of our providers so that users could download them and see first hand what our design practices are.  That work is finally availible to you.

I spend a large portion of my blogging efforts and forum responses trying to promote good designs or revealing little snippets of how we implemented the StaticSiteMapProvider and the XmlSiteMapProvider.  Now, the sources for these providers are availible for download all ASP.NET users and users can see examples of well written providers or copy the code to help bootstrap their own projects.

Download here: http://download.microsoft.com/download/a/b/3/ab3c284b-dc9a-473d-b7e3-33bacfcc8e98/ProviderToolkitSamples.msi
More Info here: http://msdn.microsoft.com/asp.net/downloads/providers/default.aspx

One note: Functionally these providers are nearly equivelent to those actually in the framework, but because they occasionally used some internal APIs, we had to alter a few calls to use public methods instead as well as some other minor changes.

Posted by dannychen | 4 Comments
Filed under:

Web Application Projects RC1 is availible!!!!

For the past few months, I've been primarily working on a new feature called Web Application Projects.  This was incredibly challenging and took way more of my time than I realized it would but it also quite exciting to work on.  So, I'm incredibly happy to finally announce that it is availible for download on msdn. 

Web Application Projects is a feature we started working on immediately after we finished shipping Visual Studio 2005.  While the "website model" is great for new projects, there were a lot of challenges for migrating Visual Studio 2003 web projects to VS05.  We also lost a few features between 03 and 05 such as being able to add class files where the logically fit in the site (ie in parallel with pages that depended on them).  We solved these problems by creating this plug-in.  It, in many ways, brings back the VS03 web project model into VS05.  Existing VS03 users should find it quite easy to pick up.

Rather than just wait for SP1 to be put together and including it there, we decided to make "WAPs" availible early as a "speedboat" or plugin for Visual Studio 2005. In the next few posts, I'll try and address some of the differences between websites and web apps.  But for now, here's a few links:

You can download it here:  http://msdn.microsoft.com/asp.net/reference/infrastructure/wap/default.aspx
You will need to patch Visual Studio in order to install WAPs, all the details are on the site.

There's a forum dedicated to WAPs here: http://forums.asp.net/1019/ShowForum.aspx

Scott Guthrie also has a lot of information about WAPs on his blog and website:
http://weblogs.asp.net/scottgu/archive/2006/04/05/442032.aspx
http://webproject.scottgu.com/
http://weblogs.asp.net/scottgu/archive/2005/12/16/433374.aspx
http://weblogs.asp.net/scottgu/archive/2006/03/27/441147.aspx
http://weblogs.asp.net/scottgu/archive/2006/02/05/437439.aspx

Posted by dannychen | 0 Comments
Filed under:

A brief history of MasterPages and templating web content (aka MasterPages are NOT frames)

One question I see constantly on the forums is:  How do I get a Masterpage to load Content1 from one page but Content2 from another page or How do I get Content2 to reload when I click a button in Content1 without reloading the entire page. 

The root of the question is often a misunderstanding of the concept of a Masterpage and confusing it with frames.  Masterpages are a way of templating a site.  By this I mean defining a look and feel for the overall site and proving a central point for common code while allowing individual pages to modify their specific content.  This is not the same thing as a frame although frames are sometimes used this way.  Instead, frames are a way of collaging multiple arbitrary pages together in a single browser.  Frames are also a bad idea [www.karlcore.com].

To begin with, I need to make the assumption that a consistent layout across various pages on a site is a good thing.  I hope this can be agreed on.  Secondly, duplication of code is a bad thing and hard to maintain.  I also hope that this can be agreed on.  The conclusion is that it would be good if the code that manages the look and feel of a site as well as the common components (such as a navigation system, header, and footer) was kept in as few and as specific as possible locations. 

To explain the motivation for Masterpages and give a little more perspective on how they were designed, let me start with an older and little seen anymore concept that was used to solve this problem.  It is called "Server Side Includes".  SSI was a relatively light weight way that a server could merge together multiple files into a single output stream to the client.  This was probably one of the first big steps into a web server actually parsing content (rather than just streaming it) and it looked something like this:

<!--#include virtual="/header.html" -->
<td>This is some content data here
</td>
<!--#include virtual="/footer.html" -->

Those aren't just html comments, they are also directives that the web server understands.  You can imagine that header.html and footer.html contained the html for a header and a footer.  Somewhere in there depending on the layout of the page was likely to be a navigation menu and perhaps copyright info, etc.  Well, this worked fine when it was 1998 but with new technology (like ASP) we wanted a bit more features.  For one thing, it would be nice of the header/footer could interact with the content and vice-versa.  So, the natural progression was to use UserControls like this:

<%@ Register Src="header.ascx" TagName="header" TagPrefix="uc1" %>
<%
@ Register Src="footer.ascx" TagName="footer" TagPrefix="uc2" %>

<uc1:header ID="Header1" runat="server" />
    <form id="form1" runat="server">
    <div>
        Some Content Here
    
</div>
    </form>
<
uc2:footer ID="Footer1" runat="server" />

Again, header markup was in header.ascx and footer markup was in footer.ascx pretty much like before.  Well, this was certainly much better than the SSIs we had before but there were still other nagging problems.  One big one was that you were almost certain to have invalid XML/HTML code in the header/footer controls.  For example, the <html> tag was likely to be opened in the header but closed in the footer leaving both files with dangling tags.  The alternative was to bring a lot of layout code into the page which defeated the purpose of this whole arrangement.

So, with Masterpages, we can address these issues (note: while I've demonstrated a problem we've solved, I don't mean to convey that this is the only problem we've tried to solve, there are certainly many others).  The Masterpage becomes the place where the layout of a site is created and where common components such as navigation can be placed.  The content pages, are exactly that, pages which contain content that is specific to the page.  And it looks something like this as most users are probably aware:

Master Page:
<%@ Master Language="VB" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<
head runat="server">
    <title>Untitled Page</title>
</
head>
<
body>
    <h1>This is my Header</h1>
    <form id="form1" runat="server">
        <div>
            <asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
                <!-- the content will get filled in here -->
            </asp:ContentPlaceHolder>          
        
</div>
    </form>
    <h2>This is my Footer</h2>
</
body>
</
html>


Content Page:
<%@ Page Language="VB" MasterPageFile="~/MasterPage.master" %>
<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
This is some content
</asp:Content>


So, in summary, I hope that a reader who has made it this far will realize that Masterpages and content pages are not frames.  Masterpages do not let you to mash many pages together in the browser.  They better than that, they are a tool for designing a website with good design practices and well formatted markup.

Posted by dannychen | 1 Comments
Filed under:

An overview of how securityTrimmingEnabled is supposed to work.

I think that the #1 most confusing or misunderstood portion of Site Navigation is the securityTrimmingEnabled flag and the roles attribute on siteMapNodes.  This post wil hopefuly clear up some of the confusion.

SecurityTrimmingEnabled

Firstly, let me be explicit about this: out of the box, securityTrimmingEnabled is meant to be a security feature.

By secure, let me use the following definitions (hopefully we agree on them):
   1) A page that should not be accessed by a user, but can actually be accessed is not secure.  Even if the page is not linked to from any other page.
   2) A page that should not be accessed and cannot be accessed is secure.

Now, consider this in the context of a web.sitemap file.  Perhaps you have a rather simple file like this:

<?xml version="1.0" encoding="UTF-8"?>
<
siteMap>
  <
siteMapNode url="home.aspx" title="Home Page - Everone can access this">
    <
siteMapNode url="links.aspx" title="Links page - Anyone can access" />
    <
siteMapNode url="admin.aspx" title="Admin Page - Only the Admin can access" />
  </
siteMapNode>
</
siteMap>

Per our definitions, the admin page must be inaccessible, not just invisible to be secure.  Therefore, simply not showing the admin link in our Menu or TreeView isn't an adequate technique.  Instead, file ACLs must be set (for Windows auth) or <location> tags for Forms auth.  Here's what web.config might look like for Forms auth:

<?xml version="1.0"?>
<
configuration>
    <
appSettings/>
    <
connectionStrings/>
    <
system.web>
      <
siteMap defaultProvider="secureProvider">
        <
providers>
          <
add name="secureProvider" type="System.Web.XmlSiteMapProvider"
              
siteMapFile="web.sitemap" securityTrimmingEnabled="true"/>
        </
providers>
      </
siteMap>
    </
system.web>

  <
location path="~/admin.aspx">
    <
system.web>
      <
authorization>
        <
allow roles="Admin"/>
        <
deny users="*"/>
      </
authorization>
    </
system.web>
  </
location>
</
configuration>

This, I would consider secure.  Because of the <location> tag, only the Admin role can access the admin.aspx page.  With securityTrimmingEnabled set to true, the navigation system will filter the admin.aspx node out from the data returned if the user is not an Admin.  You'll note that filtering was applied yet nothing was modified in the actual web.sitemap file (this is often the confusing part).  The filtering was directed by modifications to web.config and securing the site. 

The roles attribute on siteMapNodes. 

This attribute expands visibility to a particular node.  According to the previous section, a node is only visible if the user can access the node.  It is occasionally useful to show more nodes than would follow this rule.  Therefore, the SiteMapProvider additionally checks the roles attribute and, if the node wasn't already visible, will make it visible if the current user is in a role specified in the attribute.  There are two primary reasons this is useful:

   1) Nodes that don't have a url -- These nodes cannot be positively identified as accessible so they are considered inaccessible by the provider.  Therefore a role needs to be associated with them, often "*".  Ex:

<siteMapNode title="Child Pages" roles="*">
  <!--
child pages under here -->
</
siteMapNode>

   2) Common pages that require login that users would access -- Typically, by clicking on one of these links, the user will be redirected to a login page first.

<siteMapNode url="membersOnly.aspx" title="Member Access Only" roles="Users" />

What else can you do?

Some users have reported that they don't actually care about the security aspect but really want the filtering ability.  A common request is to be able to filter based on the roles attribute instead of expand based on roles.  These are fairly easy to do by extending the built in providers.  There is a single method to override.  Keep in mind that the roles collection on the SiteMapNode refers to the roles that are specified in the web.sitemap file (or when the node was constructed):

Public Class ExampleProvider
    
Inherits XmlSiteMapProvider

    
Public Overrides Function IsAccessibleToUser(ByVal context As System.Web.HttpContext, _
                                                
ByVal node As System.Web.SiteMapNode) As Boolean
        ' Write your custom logic here
    End Function

End
Class

 

Posted by dannychen | 16 Comments
Filed under:

SiteMapProvider and spaces in querystrings

A user found an interesting issue in the SiteMapProvider code.  It seems that if you have a node that has a url such as: "~/home.aspx?p=some text"  (notice the space in the query string) and you are actually navigated to that url, SiteMap.CurrentNode doesn't actually return the correct node.  The reason for this is that the url actually comes in as:  "/home.aspx?p=some%20text". 

Now, firstly, this is a bug, and it's been filed to be addressed in the future but what (if any) is the workaround?  This one is tricky because the issue is pretty deep.  You need a custom provider and the appropriate method overridden.  When the user raised the issue, I thought about it and decided I could come up with the solution in 5 minutes or let him struggle with it for an hour.  What do you think I did?

Here's the custom provider:

Public Class testProvider : Inherits XmlSiteMapProvider

    
Public Overrides Function FindSiteMapNode(ByVal context As System.Web.HttpContext) As System.Web.SiteMapNode
        
Dim node As SiteMapNode = MyBase.FindSiteMapNode(context)

        
If node Is Nothing Then
            If context Is Nothing Then
                Return Nothing
            End If

            Dim queryString As String = CType(context.CurrentHandler, Page).ClientQueryString

            
Dim pageUrl As String = HttpUtility.UrlDecode(context.Request.Path & "?" & queryString)
            node =
MyBase.FindSiteMapNode(pageUrl)
        
End If

        Return node
    
End Function

End
Class
Posted by dannychen | 2 Comments
Filed under:
 
Page view tracker