DotNetNuke: Category Administration Module

Download: CategoryAdmin_01.10.00_Install.zip (note: If using DNN4 install and run LinqPrep first. If using DNN5 follow these directions first)

Source code:

The Category Admin Module

Originally the purpose of this module was to demonstrate how to make a category administration module that allowed for unlimited nesting of categories. Then I decided to have a TreeView so I implemented the HierarchicalDataSourceControl and IHierarchicalDataSource interfaces so that the TreeView could automatically bind to the data source.

I then had to implement caching so that the database wasn't hit so many times. Now the TreeView will only hit the database once. As long as the ASP.NET app domain for the application is active it will use cached data. If you make a change to the database using the module the cache will be refreshed.

Next, I decided to implement the Category drop-down control from this tutorial to allow you to easily move a category by selecting the new parent in the drop down and clicking update.

This is when the complications started. I realized that I needed code to move the element and all the child elements. Then I added the ability to delete an element. This also required code to also readjust child elements.

So the module is no longer simple but it works and should save you a lot of time if you need this functionality. It also uses Linq to SQL.

Using The Module

When you first install the module and place it on a page in your DotNetNuke website, only the admin box will show. Enter a name for a category and click the Save button.

The category will appear in the tree to the left of the admin box. You can update the category name and click the update button or click the Add New button to add another category.

If you click the Add New button, you will now have the option to place the new category under the previous category.

If you delete a parent category...

...the child categories will automatically move up a level. You can also change the parent category in the drop down and click the Update button to move a category and all it's child categories.

The Code

All the data is stored in a simple recursive table that has only 4 fields.

For example, to represent this tree... 

The data is stored in the database like this.

The Module Code

Caching

The module uses caching to reduce the hits to the database. The caching code is contained in this file: CategoriesTable.cs. When the database is updated, the cache is refreshed using this code:

        #region RefreshCache
        private void RefreshCache()
        {
            // Get Table out of Cache
            object objCategoriesTable = HttpContext.Current.Cache.Get("CategoriesTable");

            // Is the table in the cache?
            if (objCategoriesTable != null)
            {
                // Remove table from cache
                HttpContext.Current.Cache.Remove("CategoriesTable");
            }
        }
        #endregion

Displaying the Tree

The following code is used to display the TreeView control:

            <asp:TreeView ID="tvCategories" runat="server" OnSelectedNodeChanged="tvCategories_SelectedNodeChanged"
                ExpandDepth="0">
                <SelectedNodeStyle BackColor="#CCCCCC" Font-Bold="False" Font-Underline="False" />
                <DataBindings>
                    <asp:TreeNodeBinding DataMember="ADefWebserver.Modules.CategoryAdmin.Catagories"
                        TextField="Value" ValueField="Value" Depth="0" />
                </DataBindings>
            </asp:TreeView>
This code is used in the DisplayCatagories() method to fill the TreeView control:
        private void DisplayCatagories()
        {
            CatagoriesTree colCatagories = new CatagoriesTree();
            tvCategories.DataSource = colCatagories;

            TreeNodeBinding RootBinding = new TreeNodeBinding();
            RootBinding.DataMember = "ListItem";
            RootBinding.TextField = "Text";
            RootBinding.ValueField = "Value";

            tvCategories.DataBindings.Add(RootBinding);

            tvCategories.DataBind();
            tvCategories.CollapseAll();
This file contains the code used to create the "colCatagories" data source that the TreeView binds to: CategoriesTree.cs.

Inserts and Updates

When a node is inserted or updated the following code is executed:

        #region btnUpdate_Click
        protected void btnUpdate_Click(object sender, EventArgs e)
        {
            CategoryAdminDALDataContext CategoryAdminDALDataContext = new CategoryAdminDALDataContext();

            if (btnUpdate.Text == "Update")
            {
                var result = (from AdefWebserverCategories in CategoryAdminDALDataContext.AdefWebserverCategories
                              where AdefWebserverCategories.CategoryID == Convert.ToInt32(txtCategoryID.Text)
                              select AdefWebserverCategories).FirstOrDefault();

                result.CategoryName = txtCategory.Text.Trim();

                result.ParentCategoryID = (ddlParentCategory.SelectedValue == "0") ? (int?)null : Convert.ToInt32(ddlParentCategory.SelectedValue);
                txtParentCategoryID.Text = (ddlParentCategory.SelectedValue == "0") ? "" : ddlParentCategory.SelectedValue;

                result.Level = (ddlParentCategory.SelectedValue == "0") ? 1 : GetLevelOfParent(Convert.ToInt32(ddlParentCategory.SelectedValue)) + 1;

                CategoryAdminDALDataContext.SubmitChanges();

                // Update levels off all the Children
                colProcessedCategoryIDs = new List<int>();
                UpdateLevelOfChildren(result);
            }
            else
            {
                // This is a Save for a new Node
                txtParentCategoryID.Text = (ddlParentCategory.SelectedValue == "0") ? "" : ddlParentCategory.SelectedValue;

                AdefWebserverCategory objAdefWebserverCategory = new AdefWebserverCategory();
                objAdefWebserverCategory.CategoryName = txtCategory.Text.Trim();
                objAdefWebserverCategory.ParentCategoryID = (ddlParentCategory.SelectedValue == "0") ? (int?)null : Convert.ToInt32(ddlParentCategory.SelectedValue);
                objAdefWebserverCategory.Level = (ddlParentCategory.SelectedValue == "0") ? 1 : GetLevelOfParent(Convert.ToInt32(ddlParentCategory.SelectedValue)) + 1;

                CategoryAdminDALDataContext.AdefWebserverCategories.InsertOnSubmit(objAdefWebserverCategory);
                CategoryAdminDALDataContext.SubmitChanges();

                // Set the Hidden CategoryID
                txtCategoryID.Text = objAdefWebserverCategory.CategoryID.ToString();
                ResetForm();
            }

            RefreshCache();
            DisplayCatagories();

            // Set the Parent drop-down
            if (txtParentCategoryID.Text != "")
            {
                ddlParentCategory.SelectedValue = txtParentCategoryID.Text;
            }
        }
        #endregion
The UpdateLevelOfChildren(result) method updates the child categories so that their levels move to the proper place. Note that each child category can have it's own child categories so the method must be called recursively:
        #region UpdateLevelOfChildren
        private void UpdateLevelOfChildren(AdefWebserverCategory result)
        {
            int? intStartingLevel = result.Level;

            if (colProcessedCategoryIDs == null)
            {
                colProcessedCategoryIDs = new List<int>();
            }

            CategoryAdminDALDataContext CategoryAdminDALDataContext = new CategoryAdminDALDataContext();

            // Get the children of the current item
            // This method may be called from the top level or recuresively by one of the child items
            var CategoryChildren = from AdefWebserverCategories in CategoryAdminDALDataContext.AdefWebserverCategories
                                   where AdefWebserverCategories.ParentCategoryID == result.CategoryID
                                   where !colProcessedCategoryIDs.Contains(result.CategoryID)
                                   select AdefWebserverCategories;

            if (CategoryChildren != null)
            {
                // Loop thru each item
                foreach (var objCategory in CategoryChildren)
                {
                    colProcessedCategoryIDs.Add(objCategory.CategoryID);

                    objCategory.Level = ((intStartingLevel) ?? 0) + 1;
                    CategoryAdminDALDataContext.SubmitChanges();

                    //Recursively call the UpdateLevelOfChildren method adding all children
                    UpdateLevelOfChildren(objCategory);
                }
            }
        }
        #endregion

Deleting a Category

Deleting provides a special challenge, unlike updates, when you delete a category you must alter the level and the parent of all the first level child categories. Then you must alter the level of all their child categories.

        #region btnDelete_Click
        protected void btnDelete_Click(object sender, EventArgs e)
        {
            CategoryAdminDALDataContext CategoryAdminDALDataContext = new CategoryAdminDALDataContext();

            // Get the node
            var result = (from AdefWebserverCategories in CategoryAdminDALDataContext.AdefWebserverCategories
                          where AdefWebserverCategories.CategoryID == Convert.ToInt32(txtCategoryID.Text)
                          select AdefWebserverCategories).FirstOrDefault();

            // Make a Temp object to use to update the child nodes
            AdefWebserverCategory TmpAdefWebserverCategory = new AdefWebserverCategory();
            TmpAdefWebserverCategory.CategoryID = result.CategoryID;
            if (result.ParentCategoryID == null)
            {
                TmpAdefWebserverCategory.Level = 0;
            }
            else
            {
                TmpAdefWebserverCategory.Level = GetLevelOfParent(result.ParentCategoryID);
            }

            // Delete the node
            CategoryAdminDALDataContext.AdefWebserverCategories.DeleteOnSubmit(result);
            CategoryAdminDALDataContext.SubmitChanges();

            // Update levels of all the Children            
            UpdateLevelOfChildren(TmpAdefWebserverCategory);

            // Update all the children nodes to give them a new parent
            var CategoryChildren = from AdefWebserverCategories in CategoryAdminDALDataContext.AdefWebserverCategories
                                   where AdefWebserverCategories.ParentCategoryID == result.CategoryID
                                   select AdefWebserverCategories;

            // Loop thru each item
            foreach (var objCategory in CategoryChildren)
            {
                objCategory.ParentCategoryID = result.ParentCategoryID;
                CategoryAdminDALDataContext.SubmitChanges();
            }

            RefreshCache();

            // Set the CategoryID
            txtCategoryID.Text = (result.ParentCategoryID == null) ? "" : result.ParentCategoryID.ToString();

            DisplayCatagories();
            SelectTreeNode();
        }
        #endregion

Selecting the current Category on the TreeView

When an insert, update, or deletion is executed the TreeView control reloads the new data. However, the user will want the category they were working on to be selected in the TreeView and for the node to be expanded.

You can programmatically select a node in a TreeView control by using it's Node path. The GetNodePath method accepts a CategoryID and builds a string that represents the categories current node path:

        #region GetNodePath
        private string GetNodePath(int intCategoryID)
        {
            string strNodePath = intCategoryID.ToString();

            CategoryAdminDALDataContext CategoryAdminDALDataContext = new CategoryAdminDALDataContext();

            var result = (from AdefWebserverCategories in CategoryAdminDALDataContext.AdefWebserverCategories
                          where AdefWebserverCategories.CategoryID == intCategoryID
                          select AdefWebserverCategories).FirstOrDefault();

            // Only build a node path if the current level is not the root
            if (result.Level > 1)
            {
                int intCurrentCategoryID = result.CategoryID;

                for (int i = 1; i < result.Level; i++)
                {
                    var CurrentCategory = (from AdefWebserverCategories in CategoryAdminDALDataContext.AdefWebserverCategories
                                           where AdefWebserverCategories.CategoryID == intCurrentCategoryID
                                           select AdefWebserverCategories).FirstOrDefault();

                    strNodePath = CurrentCategory.ParentCategoryID.ToString() + @"/" + strNodePath;

                    var ParentCategory = (from AdefWebserverCategories in CategoryAdminDALDataContext.AdefWebserverCategories
                                          where AdefWebserverCategories.CategoryID == CurrentCategory.ParentCategoryID
                                          select AdefWebserverCategories).FirstOrDefault();

                    intCurrentCategoryID = ParentCategory.CategoryID;
                }
            }

            return strNodePath;
        }
        #endregion

Summary

This code should be easily adaptable for your own projects. This project should demonstrate a solution to most of the problems you will encounter in implementing a nested data structure.

 

[Back to: The ADefWebserver DotNetNuke HELP WebSite]

 


Buy DotNetNuke Modules from Snowcovered

 DotNetNuke Powered!DotNetNuke is a registered trademark of DotNetNuke Corporation.