ks78
ks78

Reputation: 927

Recursive SQL to Create DNN Navigation Menu

I'm working on a DotNetNuke module that includes a tree-style navigation menu.

So far, I have it working, in the sense that child-nodes are connected to their correct parent-nodes, but the node-siblings are still out of order. There's a field called TabOrder, used to determine the order of siblings, but due to the recursion, I can't get them sorted properly.

I'm trying to do this in a SQL Server stored procedure, which may be a mistake, but I feel I'm so close that there must be a solution. Does anyone have any idea what I'm doing wrong?

I'd appreciate any ideas you have. Thanks in advance.


Solution:

I finally found a solution to my question. The key was to recursively create a Tab Lineage (TabLevel + TabOrder) from the Root Tab to the Leaf Tabs. Once that was created, I was able to order the returned records properly.

However, as I was coming back to post this I saw MarkXA's answer, which is probably the best solution. I didn't know the method GetNavigationNodes even existed.

I think he is correct that using GetNavigationNodes is a more future-proof solution, but for the time being I'll use my SQL-based solution. --What can I say? I learn the hard way.

Here it is:

ALTER procedure [dbo].[Nav_GetTabs]
    @CurrentTabID   int = 0
AS

--============================================================
--create and populate @TabLineage table variable with Tab Lineage
--
--"Lineage" consists of the concatenation of TabLevel & TabOrder, concatenated recursively from the root to leaf.
--The lineage is VERY important, making it possible to properly order the Tab links in the navigation module.
--This will be used as a lookup table to match Tabs with their lineage.
--============================================================
DECLARE @TabLineage table
    (
        TabID       int,
        Lineage     varchar(100)
    );

WITH TabLineage AS
(
    --start with root Tabs
    SELECT T.TabID, T.ParentID, CAST(REPLICATE('0', 5 - LEN(CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)))) + CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)) as varchar(100)) AS Lineage
        FROM Tabs T 
            INNER JOIN Tabs T2 ON T.TabID = T2.TabID
            INNER JOIN TabPermission TP ON T.TabID = TP.TabID
        WHERE T.ParentID IS NULL
            AND T.IsDeleted = 0 
            AND T.IsVisible = 1 
            AND TP.RoleID = -1

    UNION ALL

    --continue recursively, from parent to child Tabs
    SELECT T.TabID, T.ParentID, CAST(TL.Lineage + REPLICATE('0', 5 - LEN(CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)))) + CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)) as varchar(100)) AS Lineage
        FROM Tabs T
            INNER JOIN Tabs T2 ON T.TabID = T2.TabID
            INNER JOIN TabPermission TP ON T.TabID = TP.TabID
            INNER JOIN TabLineage TL ON T.ParentID = TL.TabID
        WHERE T.IsDeleted = 0 
            AND T.IsVisible = 1 
            AND TP.RoleID = -1
)
--insert results of recursive query into temporary table
INSERT @TabLineage
    SELECT TL.TabID, TL.Lineage FROM TabLineage TL ORDER BY TL.Lineage
    OPTION (maxrecursion 10);   --to increase number of traversed generations, increase "maxrecursion"


--============================================================
--create and populate @Ancestor table variable with @CurrentTab ancestors
--
--"Ancestors" are Tabs following the path from @CurrentTab to the root Tab it's descended from (inclusively).
--These are Tab links we want to see in the navigation.
--============================================================
DECLARE @Ancestor   table
    (
        TabID       int
    );

WITH Ancestor AS
(
    --start with @CurrentTab
    SELECT T.TabID, T.ParentID FROM Tabs T WHERE T.TabID = @CurrentTabID

    UNION ALL

    --continue recursively, from child to parent Tab
    SELECT T.TabID, T.ParentID
        FROM Ancestor A INNER JOIN Tabs T ON T.TabID = A.ParentID
)

--insert results of recursive query into temporary table
INSERT @Ancestor
    SELECT A.TabID FROM Ancestor A
    OPTION (maxrecursion 10);   --to increase number of traversed generations, increase "maxrecursion"


--============================================================
--retrieve Tabs to display in navigation

--This section UNIONs three query results together, giving us what we want:
-- 1. All Tabs at Level 0.
-- 2. All Tabs in @CurrentTab's lineage.
-- 3. All Tabs which are children of Tabs in @CurrentTab's lineage.
--============================================================
WITH TabNav (TabID, TabLevel, TabName, Lineage) AS
(
    --retrieve all Tabs at Level 0 -- (Root Tabs)
    (SELECT T.TabID, T.[Level] AS TabLevel, T.TabName, TL.Lineage
    FROM Tabs T 
        INNER JOIN TabPermission TP ON (T.TabID = TP.TabID AND TP.RoleID = -1)
        INNER JOIN @TabLineage TL ON T.TabID = TL.TabID
    WHERE T.IsDeleted = 0 
        AND T.IsVisible = 1 
        AND T.[Level] = 0

    UNION

    --retrieve Tabs in @CurrentTab's lineage
    SELECT T.TabID, T.[Level] AS TabLevel, T.TabName, TL.Lineage
    FROM Tabs T 
        INNER JOIN TabPermission TP ON (T.TabID = TP.TabID AND TP.RoleID = -1)
        INNER JOIN @Ancestor A ON T.TabID = A.TabID
        INNER JOIN @TabLineage TL ON T.TabID = TL.TabID
    WHERE T.IsDeleted = 0 
        AND T.IsVisible = 1 

    UNION

    --retrieve Tabs which are children of Tabs in @CurrentTab's lineage
    SELECT T.TabID, T.[Level] AS TabLevel, T.TabName, TL.Lineage
    FROM Tabs T 
        INNER JOIN TabPermission TP ON (T.TabID = TP.TabID AND TP.RoleID = -1)
        INNER JOIN @Ancestor A ON T.ParentID = A.TabID
        INNER JOIN @TabLineage TL ON T.TabID = TL.TabID
    WHERE T.IsDeleted = 0 
        AND T.IsVisible = 1)
)

--finally, return the Tabs to be included in the navigation module
SELECT TabID, TabLevel, TabName FROM TabNav ORDER BY Lineage;
--============================================================

Upvotes: 1

Views: 1399

Answers (2)

MarkXA
MarkXA

Reputation: 4384

The answer is "don't use SQL". There's already a method DotNetNuke.UI.Navigation.GetNavigationNodes that does this for you, and if you use it then your module won't break if and when the database schema changes. Even if you need to do something that GetNavigationNodes won't handle, you're still better off retrieving the pages via the API to be futureproof. Going directly to the database is just asking for trouble :)

Upvotes: 2

KM.
KM.

Reputation: 103637

here is a boiler plate (not based on the given OP's code) example of a recursive tree CTE, which shows how to sort a tree:

DECLARE @Contacts table (id varchar(6), first_name varchar(10), reports_to_id varchar(6))
INSERT @Contacts VALUES ('1','Jerome', NULL )  -- tree is as follows:
INSERT @Contacts VALUES ('2','Joe'   ,'1')     --                      1-Jerome
INSERT @Contacts VALUES ('3','Paul'  ,'2')     --                     /        \
INSERT @Contacts VALUES ('4','Jack'  ,'3')     --              2-Joe           9-Bill
INSERT @Contacts VALUES ('5','Daniel','3')     --            /       \              \
INSERT @Contacts VALUES ('6','David' ,'2')     --     3-Paul          6-David       10-Sam
INSERT @Contacts VALUES ('7','Ian'   ,'6')     --    /      \            /    \
INSERT @Contacts VALUES ('8','Helen' ,'6')     -- 4-Jack  5-Daniel   7-Ian    8-Helen
INSERT @Contacts VALUES ('9','Bill ' ,'1')     --
INSERT @Contacts VALUES ('10','Sam'  ,'9')     --

DECLARE @Root_id  varchar(6)

--get all nodes 2 and below
SET @Root_id=2
PRINT '@Root_id='+COALESCE(''''+@Root_id+'''','null')
;WITH StaffTree AS
(
    SELECT 
        c.id, c.first_name, c.reports_to_id, c.reports_to_id as Manager_id, cc.first_name AS Manager_first_name, 1 AS LevelOf
        FROM @Contacts                  c
            LEFT OUTER JOIN @Contacts  cc ON c.reports_to_id=cc.id
        WHERE c.id=@Root_id OR (@Root_id IS NULL AND c.reports_to_id IS NULL)
    UNION ALL
        SELECT 
            s.id, s.first_name, s.reports_to_id, t.id, t.first_name, t.LevelOf+1
        FROM StaffTree            t
            INNER JOIN @Contacts  s ON t.id=s.reports_to_id
    WHERE s.reports_to_id=@Root_id OR @Root_id IS NULL OR t.LevelOf>1
)
SELECT * FROM StaffTree ORDER BY LevelOf, first_name

OUTPUT:

@Root_id='2'
id     first_name reports_to_id Manager_id Manager_first_name LevelOf
------ ---------- ------------- ---------- ------------------ -----------
2      Joe        1             1          Jerome             1
6      David      2             2          Joe                2
3      Paul       2             2          Joe                2
5      Daniel     3             3          Paul               3
8      Helen      6             6          David              3
7      Ian        6             6          David              3
4      Jack       3             3          Paul               3

(7 row(s) affected)

The key is the LevelOf column. See how it is just a literal 1 when selecting the main parent in the CTE. The LevelOf column is then incremented in the UNION ALL portion of the recursive CTE. Each recursive call (not row) to the CTE will hit that UNION ALL one time and the increment. Not a whole lot more to it than that.

Upvotes: 1

Related Questions