Miguel A. Castro's Blog

# Monday, March 15, 2010

One of the reasons I took my blog off GeeksWithBlogs.com, despite the fact that I liked running it there for as long as I did, is so I can have source control over the site and be able to customize it as much as I want.  As before, I want people to be able to visit this site to learn about speaking engagements I do and to download session material, but rather than spread out the session material on individual blog postings (though I’ll do that as well), I wanted a single page where folks can go and download any material I’ve made available; hence www.dotnetdude.com/downloads.

So here’s where I tell you what I did and how I did it.  Now as usual, I anticipate criticism on what I did wrong and how better I could have done it and blah blah blah.  So while I really don’t care what people say about the way I do certain things, I will tell you what my goal was so you understand the methods I undertook.  I wanted a very, very simple downloads page.  Let me repeat that s-word, “SIMPLE”.  I didn’t want to have a database driven page or something with layers of categories.  I simply wanted a single page where anyone can go to get session, or magazine material.  Now that being said, I did want some amount organization while having ease of maintenance.  I simply ended up writing a page that read off a folder structure and manifested that structure in a tree.  In fact, in the interest of keeping it as simple as possible, I used Microsoft’s TreeView control.

The code is pretty generic (as you’ll soon see) and simply looks for a folder starting point, which I store in the config file.  From there it recursively iterates through the folders and files and builds the nodes of the tree.  This design let me manage the folder structure in whatever manner I want and allows for as much or as little hierarchy as I want.  I thought this was a good mix of simplicity and flexibility.

This was the easy part.  The harder part was digging around the dasBlog source code to figure out what I had to do in order to add custom pages to the site.  The dasBlog engine is very nice but does things a bit different from what conventional web forms application developers may be used to.  The site uses template files to determine the ASP.NET code for each section of a page.  For example, the main layout of every page in your blog is determined by a file called homeTemplate.blogTemplate.  This file contains what looks like conventional HTML code, and in fact it is.  However there are some keywords surrounded by <% %> tags (note, no equal sign) which will get replaced later when the template is processed and rendered.  This design actually made it pretty easy to customize the theme I chose for my blog because it is in this particular template file where I went to make alterations to the page layout.  When I wanted to customize what the blog posting area looked like, I went to the itemTemplate.blogTemplate file.  Here I could make changes like put the permalink on top instead of the bottom, or add an image to the left of my post header, etc.  Back to the homeTemplate.blogTemplate file; it is here where I added additional link tags and meta tags I wanted every page to use.  I also added the Syntax Highlighter here that you’ll see in practice below on code.  Rather than tell you a lot about this must-have utility, just check out Scott Hanselman’s post here.  Another customization I’ll save for another post is the Bing search box on the site.

So now that I knew how the templates worked, I wanted to add a custom page that appeared within the context of the main layout defined in the homeTemplate.blogTemplate file.  It didn’t take much digging into the existing pages of the site to figure out the repeating pattern they all have.

The page I wanted was to be called Downloads.aspx so I started by creating that page right off the root of my site.  Every page in dasBlog contains an empty PlaceHolder control which is later filled with a corresponding user control.

<%@ Page Language="C#" AutoEventWireup="False" CodeBehind="Downloads.aspx.cs" Inherits="newtelligence.DasBlog.Web.Downloads" %>
<asp:placeholder id="contentPlaceHolder" runat="server"></asp:placeholder>

 

The code-behind for this page follows the same pattern as other pages in the site.  The main thing is to inherit from the SharedBasePage class and implement the IPageFormatInfo interface.  The only required implementation for this interface is the BodyText property which returns a type of Control.  Here is where I would return an instance of a user control that will be the actual heart of my new page.  By instance, I mean a usage of the LoadControl command.  Another standard on the page is to expose the PlaceHolder by providing a property for it. 

Here’s the code-behind for the Downloads.aspx page:

using newtelligence.DasBlog.Runtime;
using newtelligence.DasBlog.Web.Core; 

namespace newtelligence.DasBlog.Web
{
    public partial class Downloads : SharedBasePage, IPageFormatInfo
    {
        protected override PlaceHolder ContentPlaceHolder
        {
            get { return contentPlaceHolder; }
        } 

        public Control Bodytext
        {
            get { return LoadControl("DownloadsList.ascx"); }
        }
    }
}

 

Now I had to write the user control that this page was to load and display.  This user control was simply going to have an ASP.NET TreeView control on it.

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="DownloadsList.ascx.cs" Inherits="newtelligence.DasBlog.Web.DownloadsList" %>
<%@ Register Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
    Namespace="System.Web.UI" TagPrefix="asp" %>
<div class="downloadHeading">File Download Section</div>
<div class="bodyContentStyle" id="content" runat="server">
    <div class="downloadArea">
        <asp:TreeView ID="tvwFiles" runat="server" NodeIndent="40">
            <NodeStyle CssClass="fileTree" />
        </asp:TreeView>
    </div>
</div>

 

The div-tag hierarchy is just standard div styling with which I’m sure most of you as more familiar with than I am.

The real meat of this page is in the building of the tree, but even this was not overly complex.  I simply loaded a folder starting point from my config file and started my recursive iteration.  Each time I went down a level, I sent the previously built tree node so that each subsequent child tree node can be added to its collection.  The ASP.NET TreeView control is a bit more limited than third party controls and only seems to allow node-searching through one level within the tree.  If I’m wrong on this, please let me know.

Before I show you the code for this user control, let me tell you about one more thing I came up with.  Even though a folder and file hierarchy is easy enough to follow for any of us, I wanted a tad more polishing to this page.  I wanted the option (not the requirement) to display friendly titles for both folders and files within the folders should I desire that (and I do).  To accomplish this, I knew I would need some kind of mappings file but I didn’t want one massive file to manage so what the code does is look for a file called index.xml in each folder during the recursive iteration.  This file, if it exists, will let me rename the folder itself as well as any of the contained files.

Here’s an example of the index.xml file which resides in a folder called Cairo2010:

<titles folder="Cairo Code Camp - Feb 2010">
    <title file="DynamicStateStorage.zip" name="Dynamic State Storage: An ASP.NET Provider-Based Feature" />
    <title file="Extensibility.zip" name="Extensibility: Software That Survives" />
    <title file="FunWithHandlers.zip" name="Fun With HTTP Handlers" />
    <title file="UnderstandingASPNET.zip" name="Understanding ASP.NET Under The Covers" />
</titles>

 

As you can see, based on the information in this file, the folder will display as Cairo Code Camp – Feb 2010 and each of the contained files will display as their corresponding title as show in each name attribute.  Should I forget to create one of these files for a particular folder or forget to map a particular file within a folder, or simply decide to not do it by choice, the tree will display the original folder or file name instead.

    public partial class DownloadsList : System.Web.UI.UserControl
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                string filesSubFolder = ConfigurationManager.AppSettings["files"];
                string filesFolder = Server.MapPath(filesSubFolder);
                string filesUrl = Request.ApplicationPath + "/" + filesSubFolder;

                string[] folders = ProcessFolder(filesFolder, null);
            }
        }

        private string[] ProcessFolder(string path, TreeNode parentNode)
        {
            string[] folders = Directory.GetDirectories(path);
            if (folders.Length > 0)
            {
                foreach (string folder in folders)
                {
                    DirectoryInfo dirInfo = new DirectoryInfo(folder);
                    string folderName = dirInfo.Name;

                    XDocument xmlDoc = null;

                    string indexFile = folder + "\\index.xml";
                    if (File.Exists(indexFile))
                    {
                        xmlDoc = XDocument.Load(indexFile);
                        var attrFolder = from node in xmlDoc.Descendants("titles")
                                 where node.Attribute("folder") != null && node.Attribute("folder").Value != ""
                                 select node.Attribute("folder").Value;

                        if (attrFolder != null)
                            folderName = attrFolder.FirstOrDefault<string>();
                    }

                    TreeNode folderNode = new TreeNode("&nbsp;&nbsp;" + folderName, folder);
                    folderNode.SelectAction = TreeNodeSelectAction.None;
                    folderNode.ImageUrl = "Images/Folder24.png";
                    
                    if (parentNode != null)
                        parentNode.ChildNodes.Add(folderNode);
                    else
                        tvwFiles.Nodes.Add(folderNode);

                    // retrieve files
                    string[] files = Directory.GetFiles(folder);
                    if (files.Length > 0)
                    {
                        string appPath = Request.PhysicalApplicationPath;

                        foreach (string file in files)
                        {
                            FileInfo fileInfo = new FileInfo(file);
                            string fileName = fileInfo.Name;

                            if (fileName.ToLower() != "index.xml")
                            {
                                if (xmlDoc != null)
                                {
                                    var attrName = from node in xmlDoc.Element("titles").Descendants("title")
                                                    where node.Attribute("file").Value == fileName
                                                    select node.Attribute("name").Value;

                                    if (attrName != null)
                                        fileName = attrName.FirstOrDefault<string>();
                                }

                                string fileUrl = file.Replace(appPath, "");
                                string fileAlt = fileInfo.Name;
                                string fileLink = string.Format("<a href=\"{0}\" alt=\"{1}\">{2}</a>", fileUrl, fileAlt, fileName);

                                TreeNode treeNode = new TreeNode(fileName, fileUrl);
                                treeNode.NavigateUrl = fileUrl;
                                folderNode.ChildNodes.Add(treeNode);
                            }
                        }
                    }

                    string[] subFolders = ProcessFolder(folder, folderNode);
                }
            }

            return folders;
        }
    }

 

As you can see, the code uses Linq-to-XML to access the index.xml files.

The last thing I wanted to do is to give a user the easiest URL possible so although you can navigate to www.dotnetdude.com/downloads.aspx to get to this page, I want you to be able to simply type www.dotnetdude.com/downloads.

IE7 allows for extension-less URL, my current hosting server still uses IE6 so I needed a sure-fire technique to accomplish this.  The easiest thing to do was to create a sub-folder in my site called Downloads and place an empty Default.aspx page in it.  In fact, I eliminated the code-behind for this page so it is truly empty.  I eliminated the code-behind because I didn’t need to put a code-driven redirect to my actual Downloads.aspx page.  This is thanks to a URL mapper that comes with dasBlog.  To take advantage of this, I went to the web.config file and fouind the <newtelligence.DasBlog.UrlMapper> sesion.  Following the pattern I saw in there already, I added an entry that looked like this:

<add matchExpression="(?&lt;basedir&gt;.*?)/downloads" mapTo="{basedir}/Downloads.aspx"/>

 

This should be easy enough to follow.  Suffice it to say that navigating to www.dotnetdude.com/downloads now takes you to the actual Downloads.aspx page.  In fact, I added another couple of lines to shortcut to the email page on the site and to an bio posting as well.

<add matchExpression="(?&lt;basedir&gt;.*?)/aboutme" mapTo="{basedir}/2010/03/02/AboutMeShamelessBioPost.aspx"/>
<add matchExpression="(?&lt;basedir&gt;.*?)/contact" mapTo="{basedir}/Email.aspx"/>

 

Now you can simply type www.dotnetdude.com/email to contact me, or if you’re really bored you can type www.dotnedude.com/aboutme .

The last thing I want to point out here is that the TreeView control is not using JavaScript to expand and collapse itself.  I haven’t yet figured out why as this is supposed to be a built-in feature of the ASP.NET TreeView control.  I’m suspecting that it has to do with the way dasBlog turns templates into code and renders pages.  I need to put some time into it cause I don’t really like the fact that it posts each time you collapse or expand a node.  Clicking on a file doesn’t post, since the display text for each file is literally an HTML anchor tag to the actual file to download.  I tried simply putting the control in an ASP.NET Ajax UpdatePanel control and it was ignored.  This is what tells me it’s something within dasBlog rendering since the Ajax UpdatePanel functionality is also not working.  If anyone has any ideas on this, please let me know.

I hope this has been useful to anyone who uses dasBlog and wants to customize it to their liking.  Now I just gotta fill the download page with all my session material from past event.

Until next time.

Monday, March 15, 2010 7:48:06 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0] - - Follow me on Twitter

Search
Me & My Flair

Read all about me here.
Download my Resume here.

Check out where I am here.
 
Click on logos above for profiles.
Archive
<March 2010>
SunMonTueWedThuFriSat
28123456
78910111213
14151617181920
21222324252627
28293031123
45678910
Statistics
Total Posts: 40
This Year: 0
This Month: 0
This Week: 0
Comments: 93