I ended up creating a custom module that works specifically with adjacency lists. An adjacency list in SQL is where you have a single table maintaining an "n" depth hierarchy by a column ParentId that is a foreign key back to the Id column (the primary key) in the same table.
I created a helper class called "Children". The purpose of this class is to return a list of all traversed child Id's for a given parent. So if you pass in the Id for a parent that's say 6 levels up, the below code will traverse down all 6 levels and return the complete list of Id's for all children, grandchildren, great-grandchildren, and so on.
The reason why I am getting a list of all Id's of children instead of a list of objects to delete is because if I get a list of objects to delete I would have to get these objects under a different DbContext, so when I try to actually delete them, I'll get an error saying that the object is detached (or something like that) because it was instantiated under a different context. Getting a list of Id's prevents this from happening and we can use the same context to perform the delete.
So, call this method GetChildrenIds(List<int> immediateChildrenIds)
from your code and pass in a list of ints
that correspond to the immediate children of a selected node. For example, to get the list of immediate children to pass into this method, use something like the following (this is based on an ASP.NET MVC pattern using WebAPI):
// keep in mind that `id` is the `id` of the clicked on node.
// DELETE api/region/5
public HttpResponseMessage Delete(int id)
{
Region region = db.Regions.Find(id);
List<int> tempRegionIds = new List<int>();
List<int> immediateChildrenIds = (from i in db.Regions where i.ParentId == id select i.Id).ToList();
List<int> regionsToDeleteIds = new List<int>();
// the below is needed because we need to add the Id of the clicked on node to make sure it gets deleted as well
regionsToDeleteIds.Add(region.Id);
// we can't make this a static method because that would require static member
// variables, and static member variables persist throughout each recursion
HelperClasses.HandleChildren.Children GetChildren = new HelperClasses.HandleChildren.Children();
// see below this code block for the GetChildrenIds(...) method
tempRegionIds = GetChildren.GetChildrenIds(immediateChildrenIds);
// here, we're just adding to regionsToDeleteIds the children of the traversed parent
foreach (int tempRegionId in tempRegionIds)
{
regionsToDeleteIds.Add(tempRegionId);
}
// reverse the order so it goes youngest to oldest (child to grandparent)
// is it necessary? I don't know honestly. I just wanted to make sure that
// the lowest level child got deleted first (the one that didn't have any children)
regionsToDeleteIds.Reverse(0, regionsToDeleteIds.Count);
if (regionsToDeleteIds == null)
{
return Request.CreateResponse(HttpStatusCode.NotFound);
}
foreach (int regionId in regionsToDeleteIds)
{
// here we're finding the object based on the passed in Id and deleting it
Region deleteRegion = db.Regions.Find(regionId);
db.Regions.Remove(deleteRegion);
}
...
The below code is the class that returns a complete list of children Id's. I put this code in a separate helper class file. The DbContext _db
is what I was talking about when I said you don't want to retrieve a list of objects under this context otherwise the Delete
wouldn't work when it was actually called in your controller. So instead, as you can see, I get a list of Id's instead and make the actual DbContext call to get the object inside my controller, not this helper class.
public class Children
{
private Entities _db = new Entities(HelperClasses.DBHelper.GetConnectionString());
private List<int> _childrenIds = new List<int>();
private List<int> _childRegionIds = new List<int>();
public List<int> GetChildrenIds(List<int> immediateChildrenIds)
{
// traverse the immediate children
foreach (var i in immediateChildrenIds)
{
_childRegionIds.Add(i);
_childrenIds = (from child in _db.Regions where child.ParentId == i select child.Id).ToList();
if (_childrenIds.Any())
GetChildrenIds(_childrenIds);
else
continue;
}
return _childRegionIds;
}
}