Creating a Side Bar Widget for dasBlog

I have been messing around with the dasBlog code. Going through code that others have written is a great learning experience. I wanted to extend my dasBlog site by creating a custom side bar widget and at the same time create a macro. This is very simple to do and I will attempt to describe the steps. First I will show you the results of the work and then how it was done.

Results

I wanted to create a book review widget that can be added to your side bar. The figure below shows the widget outlined by the green square. This side bar widget displays a list of the three latest books being reviewed. Each item in the list shows the following:

  • The name of the book.
  • A star rating of the book.
    • 5 stars max (gray or gold)
    • Or an “in process” indicator

image

There is also a “more…” link that direct you to a new page with additional book titles and more information for each book. To keep with the dasBlog theme, this new page inherits from the dasBlog base page and uses the “admin” template for general page layout / style.

The figure below shows the additional information available on the “more…” page. The list on the left is a complete list of books. The right hand side shows the detailed information on the book.

image

The image below shows the admin panel that is displayed. This panel works in conjunction with the list above to provide the ability to create, update and delete new entries.

image

Data Objects / Persistence

The following class diagram shows the data object that are used by the sidebar widget. Nothing too complicated. I like to keep my data objects “dumb”. There are no data access methods implemented as part of the object. Additionally, I provide a typed class for collections of objects. This makes the code more readable and the APIs easier to use.

image 

I decided to implement the data access code in a dasBlog macro object. The following class diagram shows the macro. You will find all the CRUD (create, read, update, delete) methods for the BookInfo data object in the macro class.

image I chose to persist the BookInfo data to an XML file. Serializing out the data is done in the SaveBookInfo method. The code is shown below that is used to save and read the XML data file. The XmlSerializer class is perfect for this operation. Once you know how to get the dasBlog content directory programmatically, then everything else is straight-forward.

/// <summary>
/// Saves the book info.
/// </summary>
/// <returns></returns>
public bool SaveBookInfo(BookCollection books)
{
    // Initialize return result.
    //
    bool result = false;

    // Create the root folder if necessary and get the path to the
    //    xml file that holds the book info.
    //
    string rootFolder = Path.Combine(sharedBasePage.MapPath(sharedBasePage.SiteConfig.ContentDir), "CravensBooks");
    if (!Directory.Exists(rootFolder))
    {
        Directory.CreateDirectory(rootFolder);
    }
    string bookXmlFile = Path.Combine(rootFolder, "books.xml");

    // Open up a file stream for saving.
    //
    Stream fileStream = new FileStream(bookXmlFile, FileMode.Create, FileAccess.Write);

    // Serialize out the data.
    //
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(BookCollection));
    try
    {
        xmlSerializer.Serialize(fileStream, books);
        result = true;
    }
    catch (Exception ex)
    {
    }
    finally
    {
        fileStream.Close();
    }

    return result;
}
/// <summary>
/// Loads the book info.
/// </summary>
/// <returns></returns>
public BookCollection LoadBookInfo()
{
    // Create the root folder if necessary and get the path to the
    //    xml file that holds the book info.
    //
    string rootFolder = Path.Combine(sharedBasePage.MapPath(sharedBasePage.SiteConfig.ContentDir), "CravensBooks");
    if (!Directory.Exists(rootFolder))
    {
        Directory.CreateDirectory(rootFolder);
    }
    string bookXmlFile = Path.Combine(rootFolder, "books.xml");

    // Validate the existence of the data file.
    //
    if (!File.Exists(bookXmlFile))
    {
        return new BookCollection();
    }

    // Open a stream for reading.
    //
    Stream fileStream = new FileStream(bookXmlFile, FileMode.Open, FileAccess.Read);

    // Deserialize the data.
    //
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(BookCollection));
    BookCollection books = (BookCollection)xmlSerializer.Deserialize(fileStream);

    // Close the stream.
    //
    fileStream.Close();

    // Sort by descending dates.
    //
    books.Sort(delegate(BookInfo b1, BookInfo b2) { return b2.DateAdded.CompareTo(b1.DateAdded); });

    return books;
}

As part of my learning experience, I wanted to create a dasBlog macro that can be used in the template files that define the page layout. There are plenty of resources on the web that detail how to create a dasBlog macro. I will not repeat that info here. In the above class the ShowBookControl method is a macro that renders out the sidebar widget. Here is the template markup (mostly HTML) that defines the sidebar widget.

<li>
    <h2 id="bookList">Book Review</h2>
    <div class="frame">
        <%ShowBookControl()|cravens%>
    </div>
</li>

The “<% %>” indicate a macro and the dasBlog code calls my macro class method to render out the html. The following code is the macro.

/// <summary>
/// Shows the books.
/// </summary>
/// <returns></returns>
public virtual Control ShowBookControl()
{
    // Load the book info from the xml file.
    //
    BookCollection books = LoadBookInfo();

    // Create an outer most container.
    //
    HtmlGenericControl outerDiv = new HtmlGenericControl("div");
    outerDiv.Attributes.Add("class", "bookList");

    // Create an outer container and assign it a CSS class.
    //
    HtmlGenericControl booksDiv = new HtmlGenericControl("div");
    outerDiv.Controls.Add(booksDiv);
    booksDiv.Attributes.Add("class", "books");

    // Limit the number we show in the controls section.
    //
    int maxNumberToShow = books.Count > 3 ? 3 : books.Count;

    // Show the book info and the status / review.
    //
    for (int index = 0; index < maxNumberToShow; index++)
    {
        // Get the book.
        //
        BookInfo book = books[index];

        // Create the outer book div
        //
        HtmlGenericControl bookDiv = new HtmlGenericControl("div");
        bookDiv.Attributes.Add("class", "book");
        booksDiv.Controls.Add(bookDiv);

        // Name
        //
        Label title = new Label();
        title.Text = book.Title;
        title.ToolTip = book.Title;
        bookDiv.Controls.Add(title);

        // Review
        //
        HtmlGenericControl stars = new HtmlGenericControl("div");
        stars.Attributes.Add("class", "stars");
        bookDiv.Controls.Add(stars);
        if (book.Status == BookInfo.StatusOptions.Read)
        {
            // Book has been read...add stars
            //
            int numStars = book.Review > 5? 5: book.Review;
            string starTitle = numStars.ToString() + " stars";
            for (int i = 0; i < 5; i++)
            {
                HtmlImage star = new HtmlImage();
                if (i < book.Review)
                {
                    star.Src = sharedBasePage.BlogTheme.ImageDirectory + "/bookStar.png";
                }
                else
                {
                    star.Src = sharedBasePage.BlogTheme.ImageDirectory + "/bookGrayStar.png";
                }
                star.Alt = book.Status.ToString();
                star.Attributes.Add("title", starTitle);
                stars.Controls.Add(star);
            }
        }
        else
        {
            // Not been read...add question stars
            //
            HtmlImage star = new HtmlImage();
            star.Src = sharedBasePage.BlogTheme.ImageDirectory + "/bookFuture.png";
            star.Alt = book.Status.ToString();
            star.Attributes.Add("title", book.Status.ToString());
            stars.Controls.Add(star);
        }
    }

    // Show additional info control.
    //
    HtmlGenericControl controlDiv = new HtmlGenericControl("div");
    controlDiv.Attributes.Add("class", "controls");
    outerDiv.Controls.Add(controlDiv);

    HtmlAnchor more = new HtmlAnchor();
    controlDiv.Controls.Add(more);
    more.HRef = sharedBasePage.BlogTheme.TemplateDirectory + "/BookReview.aspx";

    HtmlImage image = new HtmlImage();
    more.Controls.Add(image);
    image.Src = sharedBasePage.BlogTheme.ImageDirectory + "/bookMore.png";
    image.Attributes.Add("title", "additional info");
    image.Alt = "Additional Info";

    Label moreLabel = new Label();
    more.Controls.Add(moreLabel);
    moreLabel.Text = "more...";

    return outerDiv;
}

The macro code demonstrates how to programmatically determine the template and image directory used by your theme.

Notice the “more…” link is hooked up to a BookReview.aspx page that resides in the themes folder. This page inherits from the dasBlog base page (SharedBasePage) and implements the IPageFormatInfo interface.

image

The aspx page is essentially an empty shell. All the content for the “more..” page is defined in the BookReview user control. The user control is loaded in the  call to IPageFormatInfo’s BodyText method. This configuration allows either the “home” or “admin” dasBlog template to be used for layout / style purposes. Setting the Category property to “admin” forces the “admin” template to be used.

The SharedBasePage object has a number of useful properties and methods. It is worthwhile to set a break point in the Visual Studio debugger and explore this object. For instance, the following code is used to remove the admin content from the “more…” info page.

// Is this an admin...
//
SharedBasePage basePage = (SharedBasePage)Page;
if (! (!basePage.HideAdminTools && SiteSecurity.IsValidContributor()))
{
    // Not an admin...remove the admin div.
    //    Note: Setting Visible to false, ensures ASP.NET does
    //    not render the elements into the HTML stream.
    //
    _divAdmin.Visible = false;
}

The BookReivew user control (class diagram above) content is almost completely in the “ascx” file. The code behind page provides the event handlers for the page events and data binding for the controls.

 Improvements

I am only beginning to look at the dasBlog code base. I am certain improvements related to better ways to utilize the dasBlog code will be found. The following design improvements come to mind.

  • The “more…” page uses a ASP.NET 2.0 ListBox control. I have not tried to compile the project using ASP.NET 3.5. I assume that not much should need to be fixed. If that is true, then the ListView control would provide much more freedom.
  • The admin functions are wired up for full page post back. This provides a “flickery” experience. Nothing a little AJAX can’t fix.
  • None of the admin inputs are being validated for malicious intentions. The risk of me hacking my own site is small. But this is sloppy and should be corrected.
Tags:

Leave a Reply

Your email address will not be published.

*