Dynamic Display of Uploaded Content in MasterPages

by Peter A. Bromberg, Ph.D.

Peter Bromberg
"Suppose you were an idiot and suppose you were a member of Congress. But I repeat myself."  - - Mark Twain

This article is a tutorial about uploading content, storing details in a database, and merging uploaded files into a MasterPage, a technique that I suspect a lot of beginning ASP.NET 2.0 developers would like to be able to do, and on which I have not seen any information as of this writing.

The basic concept is this:

  • You want to be able to upload a pre-formatted Html snippet of an article or some similar content.
  • You want to be able have this file deposited on the filesystem and an appropriate entry inserted into your database with the content subject, description, filename, category, and published date.
  • You want your "Articles" listing page to be automatically updated with the appropriate listing and a link to the new article.
  • You want a "template" "article displayer" page that will automatically retrieve the specified content from its place on the filesystem and merge it into your MasterPage for display to the user, so it seamlessly fits in with your site.

In other words, the beginnings of a simplified CMS or "content management system" for a website that features articles. Now there are several ways to do this; you could choose to store the actual content in the database too, but for this "proof of concept" I've chosen only to store information about the resource in the database, and to store the content itself on the filesystem. Bear in mind that there are two ways to store content in files - you can keep them in a subfolder of the IIS VRoot that your app runs in, or you can store them in a folder completely outside of your IIS app, even on a completely different hard drive if desired. The technique I illustrate here will work with either choice, and only one line of code needs to be changed.



This is really a very simple solution as in essence it only requires three ASPX pages - an "Default" page that displays the listing of current articles with category, description and a link to bring up the article, an "Upload" page that is used by the site administrator (or even by users) to upload a new article, and an "Article" page that is used as the template to display any requested resource. Of course, we need a MasterPage, and for this example I've simply chosen one of the "Stock" free templates available from the Microsofties at ASP.NET, and merged the HTML and images into a MasterPage. The final product, in case you are interested, is the one with the picture of the "Man on the Bridge":

"Man on a Bridge"... Certainly this has some existential, metaphysical higher-Karma meaning. Whatever, maybe it's Brooklyn and the guy just bought the bridge? Who knows, it escapes me for now...

So, let's get to work. First, we need to create our database tables and stored procs for our little CMS. We'll have two tables, a Categories table with CategoryID and CategoryName fields, and a related Articles table with ID, CategoryID, title, pubDate, description and fileName fields. We will have three stored procedures, GetCategories, GetArticles, and AddArticle. No big deal here, the sprocs are so simple I won't even take up the space to list them -- all this stuff is in the download anyway.

The main "Default" page will be the one that displays our list of Articles with links to each. For this, I've chosen a Repeater control as it provides a simple way to provide some HTML in a template and repeatedly bind various elements to the fields for each row in a returned data source. The template looks like this:

<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<center><h3>Articles</h3></center>
<ul>
<asp:Repeater ID="Repeater1" runat="server">
<ItemTemplate>
<li>
Category:&nbsp;<%# Eval("categoryName") %><Br />
<a href="article.aspx?page=<%# Eval("filename" )%>&title=<%#Eval("title" ) %>"><%# Eval("title") %></a>
<BR />
<%# Eval("description") %>
</li>
</ItemTemplate>
</asp:Repeater>
</ul>
</asp:Content>

The result of this markup can be seen in the screen cap pic above.

Now, let's switch over to the codebehind and see how this gets populated:

 

public partial class _Default : System.Web.UI.Page

               {

                   protected void Page_Load(object sender, EventArgs e)

                   {

                       DataTable dt =GetArticlesDt();

                       this.Repeater1.DataSource = dt;

                       Repeater1.DataBind();

                   }

                   private DataTable GetArticlesDt()

                   {

                       DataTable dt = null;

                       if (Cache["articlesDt"] == null)

                       {

                           DataSet ds = new DataSet();

                           string connectionString =                            System.Configuration.ConfigurationManager.AppSettings["connectionString"];

                           SqlCommand cmd = new SqlCommand("dbo.GetArticles");

                           cmd.CommandType = CommandType.StoredProcedure;

                           SqlConnection cn = new SqlConnection(connectionString);

                           cmd.Connection = cn;

                           SqlDataAdapter ad = new SqlDataAdapter(cmd);

                           ad.Fill(ds);

                           dt = ds.Tables[0];

                           Cache.Insert("articlesDt", dt, null, System.Web.Caching.Cache.NoAbsoluteExpiration,

                               TimeSpan.FromMinutes(30));   

                       }

                       else

                       {

                           dt = (DataTable)Cache["articlesDt"];

                       }

                       return dt;

                   }

You see I have a method "GetArticlesDt" that automatically checks the Cache and ensures that we always can get our DataTable out of Cache in order to avoid repetitive Database calls. Then we just set the DataSource on the Repeater and DataBind. That was easy!

As an aside, I want to mention that I use Caching on almost all my work; at Tech-Ed 2004 Rob Howard gave a presentation on caching that totally blew me away and the lesson was learned very well. Caching data for as little as one second can have a dramatic effect on throughput of as much as 500 percent. This may not seem particularly important if you are just starting out, but I can tell you that when you've got a successful web site with millions of hits per month, you better pay attention to caching! There are lots of different ways to cache data and even whole or partial pages and controls; a whole book could be written on these techniques.

Now we will switch over to review the the upload page, and finally to the Article display page:

The upload page has TextBoxes or Textareas for title and description, a dropdown for the Category that is populated via a caching method identical to the Default page, and a FileUpload control to upload the article html file. There are also Required Field validators on the Title, description, and Upload File controls that prevent a user from submitting an incomplete form. A look at the most important part of the codebehind:

protected void btnSubmit_Click(object sender, EventArgs e)

    {

        SqlConnection cn = null;

        int ret = 0;

        try

        {

            string fileName = this.FileUpload1.PostedFile.FileName;

            fileName=fileName.Substring(fileName.LastIndexOf("\\")+1);

            HttpPostedFile fil = FileUpload1.PostedFile;

            string articlePath = System.Configuration.ConfigurationManager.AppSettings["articleFolder"].ToString();

            string fullPath = articlePath + "/" + fileName;

            fil.SaveAs(Server.MapPath(fullPath));

            string connectionString = System.Configuration.ConfigurationManager.AppSettings["connectionString"];

            SqlCommand cmd = new SqlCommand("dbo.AddArticle");

            cmd.CommandType = CommandType.StoredProcedure;

            cn = new SqlConnection(connectionString);

            cmd.Connection = cn;           

            cmd.Parameters.AddWithValue("@CategoryID", this.DropDownList1.SelectedValue);

            cmd.Parameters.AddWithValue("@title", this.txtTitle.Text);

            cmd.Parameters.AddWithValue("@description", this.txtDescription.Text);

            cmd.Parameters.AddWithValue("@fileName", fileName);

            cn.Open();

            ret = cmd.ExecuteNonQuery();

        }

        catch (Exception ex)

        {

            this.lblMessage.Text = ex.Message;

        }

        finally

        {

            cn.Close();

        }

 

        if (ret > 0)

        {           

            Cache.Remove("articlesDt"); // force Cache "restock" for main page

            Server.Transfer("Default.aspx", true);

        }

        else

        {

            lblMessage.Text = "UH-OH, Wha Hoppen?";

        }

    }

We get the filename from the FileUpload control, "fix it" by triming backslashes, get the articleFolder from our appSettings section in config, create a full path, then SAVE the uploaded file in the /articles folder. Finally, we insert the new row in the Articles table using the dbo.AddArticle stored proc, and finally, we Remove the "articlesDt" Cache item, forcing a reload of the refreshed datatable when we Server.Transfer back to our Default page.

And that leaves us with the Article page, where I'll only need to illustrate the codebehind method used to "merge" the file from the filesystem into the MasterPage:

protected void Page_Load(object sender, EventArgs e)

{

string articlePath =

System.Configuration.ConfigurationManager.AppSettings["articleFolder"].ToString();

if (Request.QueryString["page"] == null) Server.Transfer("Default.aspx");

string title = Request.QueryString["title"].ToString();

string pageName = Request.QueryString["page"].Replace("\"","");

string fullPath = articlePath +"/"+ pageName;  

HtmlGenericControl thegen = new HtmlGenericControl();

        // use next line if using fixed path outside of the web, e.g. "C:\articles\article.htm"

        //    StreamReader rdr = new StreamReader(pageName);

        // use this line if local vroot path inside IIS app:

if( !File.Exists(Server.MapPath(fullPath)) )

Server.Transfer("Default.aspx");

StreamReader rdr = null;

try

{

 

rdr = new StreamReader(Server.MapPath(fullPath));

}

catch

{

rdr.Close();

Server.Transfer("Default.aspx");

}   

string content = rdr.ReadToEnd();

rdr.Close();       

thegen.InnerHtml = content;

this.Panel1.Controls.Add(thegen);

this.Title = title;

}

Here you can see that our hyperlink from the "default" page that lists the articles has placed both the article Title and the name of the physical page (file) onto the QueryString. In this manner, the same page can be re-used to display any content. We simply do some error checking, read the content into a string with a StreamReader, assign it to a new HtmlGenericControl, and add the control to the Panel that sits in our ContentPlaceHolder for the page. The result is seamlessly merged into the MasterPage. Should you choose to keep your content outside of your web where Server.MapPath would not work, you would simply provide the full physical path (e.g., "C:\Content\Article1.htm") to your StreamReader and read it right in exactly the same way. In the event of a "boo-boo" I just Server.Transfer back to the Default page. And that is pretty much it!

In the download below, I have a SQL subfolder containing a script that will create the two tables and the sprocs, and load some simple default data. There are also the files for the two articles which were simply taken from one of those "Free articles" sites. The articles of course, need to be formatted so that their width (550px in this case) will fit inside the ContentPlaceholder of your MasterPage, or it won't look very pretty. Finally, ensure that the connectionString setting in the appSettings section of the web.config matches your environment, or you won't get very far with the sample. Enjoy!

Download the Visual Studio.NET 2005 solution that accompanies this article

 

Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: