About this article

Written by:
Posted:
12/08/2010 11:39:10

About the author

Imar Spaanjaars is the owner of De Vier Koeden, a company specializing in consultancy and development on the Microsoft .NET platform and DynamicWeb.

Interested in custom development or consultancy on DynamicWeb or .NET? Then contact De Vier Koeden through the Contact page.

If you want to find out more about Imar Spaanjaars, check out his About page on this web site or check out his personal website.

Custom Modules - Part 7 - Dealing with User Input

In part 6 of this article series on building custom modules for Dynamicweb you learned how to create loops to display repeating content. You also saw how to use nested loops to create a double list with all categories and their associated articles. But what if you wanted to let the user choose a specific category for which they want to display the articles? Or what if you want to let the user search for articles that match certain criteria? That's when you need to deal with user input.

If you are familiar with "classic" ASP, ASP.NET MVC or PHP, you probably find it easy to work with user input in Dynamicweb's front-end classes. However, if you are used to ASP.NET 2.0 or later, you may be a bit surprised about the way Dynamicweb deals with input coming from web forms. Because of the architecture of Dynamicweb, concepts like postbacks and the Page Life Cycle are not supported in front-end pages (unless you are using User Controls). Instead, you need to rely on stuff like Request.QueryString and Request.Form to get data out of a web page. Since the ContentModule class does not inherit from Page as normal ASPX pages do, you cannot access the Request object directly. Instead, you need to use HttpContext.Current, like this:

string categoryName = HttpContext.Current.Request.QueryString.Get("CategoryName");
string selectedCategory = HttpContext.Current.Request.Form.Get("CategoriesList")

Alternatively, you can use Dynamicweb's Base.Request which searches Request.QueryString, Request.Form, Request.Cookies and Request.ServerVariables (in that order, as it it uses the standard ASP.NET Request object internally) but as an added benefit also cleans the returned value from possible SQL Injection attack code. You should use this alternative by default, or make sure you check for invalid data yourself to protect your system.

Processing User Input

To show you how to take user input from the query string and form collections and how to dynamically adapt the data presented in the module based on user input, I'll modify the Frontend class for the custom Articles modules by adding the following features:

  • Display all categories in the database in a drop down so the user can select one.
  • When a new category is selected, the page posts back and the articles for the chosen category are displayed.
  • Display the details of an article when a user clicks the title of an article in the article list.

I'll start with the last bullet point. I'll take an easy way out and simply stream back the title and body of the article wrapped in some HTML tags. In a real world example, you would use the same mechanisms as used for the article list: create a separate template with appropriate template tags and modify the module's Edit page to enable a user to select a custom template for the details page. You'll see how to implement this in part 8 of this article series.

  1. In the GetContent method of the Frontend class (in Frontend.cs), I wrapped all code inside an if statement and added the following code to the else block:
    if (Base.ChkInteger(Base.Request("ArticleId")) == 0)
    {
      ... original source here
    }
    else
    {
      int articleId = Base.ChkInteger(Base.Request("ArticleId"));
      using (CustomModulesSamplesEntities db = new CustomModulesSamplesEntities())
      {
        var article = db.Articles.Where(
             a => a.PublicationDateFrom <= DateTime.Now 
                  && a.PublicationDateTo >= DateTime.Now 
                  && !a.Deleted 
                  && a.Id == articleId).FirstOrDefault();
        if (article != null)
        {
          return string.Format("<h1>{0}</h1><p>{1}</p>",
                  article.Title, article.Body);
        }
        else
        {
          return "This article could not be found.";
        }
      }
    }

    As you can see, this code uses Base.Request to get values from the current HttpContext which in turn provides access to things like the QueryString and Forms collections. Using this approach you can let Dynamicweb handle dangerous values to avoid SQL injections attempts from the client. Again, in a real-world application, you would not return HTML like this directly, but use a Template object and appropriate template tags instead. However, in this article the focus is on showing you how to deal with user input, so I decided to make things a little simpler and not bother you with the template code.

    When you now click the link of an article in the list in the browser, you should see its details appear.

  2. Next, to filter the list of articles presented on the page to those from a specific category, I am going to display a drop down list with the categories. To implement this, I created a new template file called ArticleListWithFilter.html which has most of the code from the earlier ArticleList.html file but now also displays a drop-down list above the list of articles:
    <h1>Articles</h1>
    <form name="CategoryList" action="<!--@Global:Pageview.Url.Raw-->" method="post">
      <select id="CategoryId" name="CategoryId" 
           onchange="document.forms['CategoryList'].submit();">
        <option value="">Select a category</option>
        <!--@LoopStart(Categories)-->
        <option value="<!--@Id-->"><!--@Description--></option>
        <!--@LoopEnd(Categories)-->
      </select>
      <!--@If Defined(CategoryName)-->
        <h2>Articles in <!--@CategoryName--></h2>
      <!--@EndIf(CategoryName)-->
      <!--@LoopStart(ItemsLoop)-->
        <!--@EmptyLoopStart-->
          No articles found in the selected category.
        <!--@EmptyLoopEnd-->
        <!--@HeaderStart--><ul><!--@HeaderEnd-->
          <li>
            <a href="<!--@DetailsUrl-->"><!--@Title--></a>
          </li>
        <!--@FooterStart--></ul><!--@FooterEnd-->
      <!--@LoopEnd(ItemsLoop)-->
    </form>                

    This code defines two loops: one for the categories and one for the actual articles. The entire block of code is wrapped in a pair of <form> tags and the drop-down is set to post back to the server automatically when the user changes the category in the drop-down.

  3. I then modified the if block in the GetContent method as follow:
    if (Base.ChkInteger(Base.Request("ArticleId")) == 0)
    {
      int categoryId = Base.ChkInteger(Base.Request("CategoryId"));
      Dynamicweb.Templatev2.Template listTemplate = 
        new Dynamicweb.Templatev2.Template("DvkArticles/ArticleListWithFilter.html");
      using (CustomModulesSamplesEntities db = new CustomModulesSamplesEntities())
      {
        if (categoryId > 0)
        {
          var category = db.Categories.Include("Articles").Where(
               c => c.Id == categoryId).FirstOrDefault();
          if (category == null)
          {
            return "Unknown category";
          }
          listTemplate.SetTag("CategoryName", category.Description);
          Dynamicweb.Templatev2.Template articleTemplate = 
                listTemplate.GetLoop("ItemsLoop");
          foreach (var article in category.Articles.Where(
                 c => (c.PublicationDateFrom <= DateTime.Now 
                 && c.PublicationDateTo >= DateTime.Now 
                 && !c.Deleted)).OrderBy(c => c.Title))
          {
            articleTemplate.SetTag("Title", article.Title);
            string url = string.Format("Default.aspx?ID={0}&ArticleId={1}", 
                        Pageview.ID.ToString(), article.Id.ToString());
            string link = SearchEngineFriendlyURLs.getSearchFriendlyUrl(
                         url, article.Title);
            articleTemplate.SetTag("DetailsUrl", link);
            articleTemplate.CommitLoop();
          }
        }
        Dynamicweb.Templatev2.Template categoryLoop = 
                   listTemplate.GetLoop("Categories");
        foreach (Category category in db.Categories)
        {
          categoryLoop.SetTag("Id", category.Id);
          categoryLoop.SetTag("Description", category.Description);
          categoryLoop.CommitLoop();
        }
        return listTemplate.Output();
      }
    }
    ...

    The code first checks if a valid category was requested (from the <select /> element defined in the <form /> in ArticleListWithFilter.html. It does this by querying the entities model for the requested categoryId which was retrieved using Base.ChkInteger(Base.Request("CategoryId")). It then queries the selected category, outputs its name and then loops over its active and non-deleted articles, similar to what you've seen before. At the end of the method, the code also outputs a loop for all the available categories in the system. You need to do this regardless the value of categoryId, or the categories only show up the first time.

  4. When you now request the article page in your browser, you can choose a category from the drop down list. The page reloads and shows you only the articles from the selected category. When you click the title of an article you'll see the full details page.

Summary

In this short article you saw how to handle use input coming from the query string or from a form. In both cases, you can use Base.Request to access the selected values. Using methods such as Base.Request and Base.ChkInteger you can access user supplied data in a safe way and prevent SQL injection attacks. If you need more control over the source of the data, you can use HttpContext.Current and access its properties such as Request and Response as you normally would.

In the next article, you find a tip to make accessing collections such as Request and Response a little easier by implementing a base class for your custom modules.

Downloads

With each article, I'll make a download available with the module I've built so far. Additionally, you can download the full running demo with all changes up to the latest part in the series. The first download always shows the code from the article you're reading, while the second download is the full example and may contain code that is added or changed in later parts of the series.

By clicking 'Accept All' you consent that we may collect information about you for various purposes, including: Statistics and Marketing