Articles

Using XSLT to display an ASP.net sitemap without using tables

The quick and easy way of displaying an ASP.net site map (web.sitemap) in an ASP.net page is to use a TreeView control bound to a SiteMapDataSource component as shown in the following example:

<asp:SiteMapDataSource runat="server" ID="siteMapDataSource" EnableViewState="False"   ShowStartingNode="False" />
<asp:TreeView runat="server" ID="siteMapTreeView" DataSourceID="siteMapDataSource"  EnableClientScript="False" EnableViewState="False" ShowExpandCollapse="False"></asp:TreeView>

Which results in a mass of nested tables, in-line styles, and generally messy mark-up.

With just a little more effort however, you can display the sitemap using a XSLT transform, resulting in slim, clean and configurable mark-up - and not a table to be seen.

This approach can be used with both Web Forms and MVC.

This article assumes you already have a pre-made ASP.net sitemap file.

Defining the XSLT

Add a new XSLT File to your project. In this case, it's named sitemap.xslt.

Next, paste in the mark-up below.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:map="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" exclude-result-prefixes="map">
  <xsl:output method="xml" encoding="utf-8" indent="yes"/>

  <xsl:template name="mapNode" match="map:siteMap">
    <ul>
      <xsl:apply-templates/>
    </ul>
  </xsl:template>

  <xsl:template match="map:siteMapNode">
    <li>
      <a href="http://cyotek.com{substring(@url, 2)}" title="{@description}">
        <xsl:value-of select="@title"/>
      </a>

      <xsl:if test="map:siteMapNode">
        <xsl:call-template name="mapNode"/>
      </xsl:if>

    </li>
  </xsl:template>

</xsl:stylesheet>

Note:
As generally all URL's in ASP.net site maps start with ~/, the href tag in the above example has been customized to include the domain http://cyotek.com at the start, then use the XSLT substring function to strip the ~/ from the start of the URL. Don't forget to modify the URL to point to your own domain!

Declaratively transforming the document

If you are using Web forms controls, then this may be the more convenient approach for you.

Just add the XML component to your page, and set the DocumentSource property to the name of the sitemap, and the TransformSource property to the name of your XSLT file.

<asp:Xml runat="server" ID="xmlSiteMapViewer" DocumentSource="~/web.sitemap" TransformSource="~/sitemap.xslt" />

Programmatically transforming the document

The ASP.net XML control doesn't need to be inside a server side form tag, so you can use the exact same code above in your MVC views.

However, if you want to do this programmatically, the following code works too.

  var xmlFileName = Server.MapPath("~/web.sitemap");
  var xslFileName=Server.MapPath("~/sitemap.xslt");
  var result =new System.IO.StringWriter();
  var transform = new System.Xml.Xsl.XslCompiledTransform();

  transform.Load(xslFileName);
  transform.Transform(xmlFileName, null, result);

  Response.Write(result.ToString());

The result

The output of the transform will be simple series of nested unordered lists, clean and ready to be styled with CSS. And for little more effort than it took to do the original tree view solution.

With a bit more tweaking you can probably expand this to show only a single branch, useful for navigation within a section of a website, or creating breadcrumb trails.

Bookmark and Share kick it on DotNetKicks.com
Comment Post a Comment | Permalink Permalink

Error 80040154 when trying to use SourceSafe via interop on 64bit Windows

We recently moved to Windows 7, and I decided to go with the 64bit version for my machine. One of the utilities we use is a small tool for adding folders to Visual SourceSafe (why we haven't moved to another SCC provider yet is another question!) via the SourceSafeTypeLib interop dll. However, I was most annoyed when it wouldn't work on my machine, the following exception message would be displayed:

Retrieving the COM class factory for component with CLSID {783CD4E4-9D54-11CF-B8EE-00608CC9A71F} failed due to the following error: 80040154.

By default, .NET applications run using the CLR that matches your operating system, ie x64 on Windows 64bit, and x86 on Windows 32bit. I found that if I change the platform target from Any CPU to x86 (you can find this on the Build tab of your project's properties) to force it to use the 32bit CLR, then the interop would succeed and the utility would work again.

Hopefully this will be of use for the next person with this problem. Meanwhile I'm still thinking about a new SCC provider :)

Bookmark and Share kick it on DotNetKicks.com
Comment Post a Comment | Permalink Permalink

Creating a GroupBox containing an image and a custom display rectangle


One of our applications required a GroupBox which was more like the one featured in the Options dialog of Microsoft Outlook 2003. This article describes how to create a custom GroupBox component which allows this type of user interface, and also a neat trick on adjusting the client area so that when you drag controls inside the GroupBox, the handy little margin guides allow you to position without overlapping the icon.

Add a new Component class to your project, and inherit this from the standard GroupBox.

[ToolboxItem(true)]
[DefaultEvent("Click"), DefaultProperty("Text")]
public partial class GroupBox : System.Windows.Forms.GroupBox

I personally don't like assigning variables at the same time as defining them, so I've added a default constructor to assign the defaults and also to set-up the component as we need to set a few ControlStyles.

    public GroupBox()
    {
      _iconMargin = new Size(0, 6);
      _lineColorBottom = SystemColors.ButtonHighlight;
      _lineColorTop = SystemColors.ButtonShadow;

      this.SetStyle(ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw |
                    ControlStyles.UserPaint | ControlStyles.SupportsTransparentBackColor, true);

      this.CreateResources();
    }

Although this is a simple component, we need at the minimum an Image property to specify the image. We're also adding color properties in case we decide to use the component in a non-standard interface later on.

    private Size _iconMargin;
    private Image _image;
    private Color _lineColorBottom;
    private Color _lineColorTop;

    [Category("Appearance"), DefaultValue(typeof(Size), "0, 6")]
    public Size IconMargin
    {
      get { return _iconMargin; }
      set
      {
        _iconMargin = value;
        this.Invalidate();
      }
    }

    [Category("Appearance"), DefaultValue(typeof(Image), "")]
    public Image Image
    {
      get { return _image; }
      set
      {
        _image = value;
        this.Invalidate();
      }
    }

    [Category("Appearance"), DefaultValue(typeof(Color), "ButtonHighlight")]
    public Color LineColorBottom
    {
      get { return _lineColorBottom; }
      set
      {
        _lineColorBottom = value;
        this.CreateResources();
        this.Invalidate();
      }
    }

    [Category("Appearance"), DefaultValue(typeof(Color), "ButtonShadow")]
    public Color LineColorTop
    {
      get { return _lineColorTop; }
      set
      {
        _lineColorTop = value;
        this.CreateResources();
        this.Invalidate();
      }
    }

    [DefaultValue("")]
    public override string Text
    {
      get { return base.Text; }
      set
      {
        base.Text = value;
        this.Invalidate();
      }
    }

If you wanted you could create and destroy required GDI objects every time the control is painted, but in this example I've opted to create them once for the lifetime of the control. Therefore I've added CreateResources and CleanUpResources to create and destroy these. Although not demonstrated in this in-line listing, CleanUpResources is also called from the components Dispose method. You'll also notice CreateResources is called whenever a property value changes, and that it first releases resources in use.

    private void CleanUpResources()
    {
      if (_topPen != null)
        _topPen.Dispose();

      if (_bottomPen != null)
        _bottomPen.Dispose();

      if (_textBrush != null)
        _textBrush.Dispose();
    }

    private void CreateResources()
    {
      this.CleanUpResources();

      _topPen = new Pen(_lineColorTop);
      _bottomPen = new Pen(_lineColorBottom);
      _textBrush = new SolidBrush(this.ForeColor);
    }

Now that all the initialization is performed, we're going to add our drawing routine which is to simply override the OnPaint method.

Remember that as we are overriding an existing component, we should override the base components methods whenever possible - this means overriding OnPaint and not hooking into the Paint event.

    protected override void OnPaint(PaintEventArgs e)
    {
      SizeF size;
      int y;

      size = e.Graphics.MeasureString(this.Text, this.Font);
      y = (int)(size.Height + 3) / 2;

      // draw the header text and line
      e.Graphics.DrawString(this.Text, this.Font, _textBrush, 1, 1);
      e.Graphics.DrawLine(_topPen, size.Width + 3, y, this.Width - 5, y);
      e.Graphics.DrawLine(_bottomPen, size.Width + 3, y + 1, this.Width - 5, y + 1);

      // draw the image
      if ((_image != null))
        e.Graphics.DrawImage(_image, this.Padding.Left + _iconMargin.Width, this.Padding.Top + (int)size.Height + _iconMargin.Height, _image.Width, _image.Height);

      //draw a designtime outline
      if (this.DesignMode)
      {
        Pen pen;
        pen = new Pen(SystemColors.ButtonShadow);
        pen.DashStyle = DashStyle.Dot;
        e.Graphics.DrawRectangle(pen, 0, 0, Width - 1, Height - 1);
        pen.Dispose();
      }
    }

In the code above you'll also notice a block specifically for design time. As this control only has borders at the top of the control, at design time it may not be obvious where the boundaries of the control are when laying out your interface. This code adds a dotted outline to the control at design time, and is ignored at runtime.

Another method we are overriding is OnSystemColorsChanged. As our default colors are based on system colors, should these change we need to recreate our objects and repaint the control.

    protected override void OnSystemColorsChanged(EventArgs e)
    {
      base.OnSystemColorsChanged(e);

      this.CreateResources();
      this.Invalidate();
    }


The client area of a standard group box accounts for the text header and the borders. Our component however, needs an additional offset on the left to account for the icon. If you try and place controls into the group box, you will see the snapping guides appear in the "wrong" place.

Fortunately however, it is very easy for us to suggest our own client area via the DisplayRectangle property. We just override this and provide a new rectangle which includes provisions for the width of the image.

    public override Rectangle DisplayRectangle
    {
      get
      {
        Size clientSize;
        int fontHeight;
        int imageSize;

        clientSize = base.ClientSize;
        fontHeight = this.Font.Height;

        if (_image != null)
          imageSize = _iconMargin.Width + _image.Width + 3;
        else
          imageSize = 0;

        return new Rectangle(3 + imageSize, fontHeight + 3, Math.Max(clientSize.Width - (imageSize + 6), 0), Math.Max((clientSize.Height - fontHeight) - 6, 0));
      }
    }


Now as you can see the snapping guides suggest a suitable left margin based on the current image width.

You can download the complete source for the GroupBox component below.

Bookmark and Share kick it on DotNetKicks.com

The following files are associated with this article:

Comment Post a Comment | Permalink Permalink
  • Page 1 of 1
  • |
  • « Previous
  • 1
  • Next »