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:
App_Code
CategoryAdmin
DesktopModules
CategoryAdmin
Providers
DataProviders
SqlDataProvider
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.
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.
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 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
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.
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; } } #endregionThe 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 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
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
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™ is a registered trademark of DotNetNuke Corporation.