Generate submenu links from Parent ChildNodes of different Page Types

Stuart1 Freeman1 asked on January 6, 2020 22:55

Hi All,

My project is version 12.xx (latest) and MVC. I am attempting to dynamically build submenus for pages with childpages/nodes. I am using the modelprovider->model->repository model to make strongly typed DocumentQueries. I am also using the caching strategy used in the Dancing Goat MVC example.

So my question is how can I generate a list of links(url and text) from a typed page's child nodes/pages? All the child nodes are varying types but all include a PageName and the NodeAliasPath. I've tried creating my own PageChildLinks model and viewmodel but when I attempt to cast the TreeNodes to ChildLink model I get an error which i assume is happening because the type does not exist in the CMS - I don't want it to, I just want to use the child TreeNodes to populate my ChildLink's type on the CMS side.

I'm open to whatever strategy is required to get this to work. Maybe I am going about it the wrong way? Ideally I can put the query in a repository and cache it like my other strongly typed document queries. And without having to create and manage the Page Type in the CMS.

Thanks!

** Edit **

I wasn't completely clear but the child pages are of varying types but all include some common fields like QuickLinkIcon, MenuItemLinkText. It's probably possible to utilize the custom Page Type inheritance to make a strongly typed query against all these different pages for those fields they have in common? What I did was create a custom page type with these fields that contains no records and this allowed me to make these queries and cast the results to that type.

Correct Answer

Trevor Fayas answered on January 13, 2020 16:54

The boilerplate has an older rendition of dynamic routing, I haven't had time to update it I'm afraid, so you don't need any of the routing stuff in there and instead use the dynamic routing module. I'll try to get it updated sometime this week, I'm still working on our own baseline starting points.

Perhaps a quick screen share meeting would get you going faster, email me and I'll set up a time to help you get started. You can use my work email tfayas@hbs.net

0 votesVote for this answer Unmark Correct answer

Recent Answers


Dmitry Bastron answered on January 7, 2020 10:27

Hi!

If I got you right, you are nearly there. You need a simple view model:

public class LinkViewModel
{
    public string PageName { get; set; }
    public string Url { get; set; }

    public LinkViewModel(TreeNode document)
    {
        PageName = document.DocumentName;
        Url = URLHelper.ResolveUrl(document.RelativeURL);
    }
}

And then you can get a list of these links from your parent document and add some caching:

var linkViewModels = parentDocument.Children.Select(x => new LinkViewModel(x));
3 votesVote for this answer Mark as a Correct answer

Trevor Fayas answered on January 8, 2020 16:22

I'm working on the same thing, Dmitry has the correct idea, i plan on hopefully sharing some of the code in a blog article once all done.

If you're using the IRepository stuff, check out the MVC Caching package on the marketplace, uses and improves on the dancing goat Caching system.

1 votesVote for this answer Mark as a Correct answer

Stuart1 Freeman1 answered on January 9, 2020 15:38

Hi Dmitry, thanks for your insight. Maybe I made this more difficult than it should be. I am trying to follow Dancing goat using repositories and dependency injection. In your example:

var linkViewModels = parentDocument.Children.Select(x => new LinkViewModel(x));

Would you do this in the controller? And so request the parent document treenode in the controller from my repository? How does your example trigger caching? I'm new to Kentico. Thanks for your help!

0 votesVote for this answer Mark as a Correct answer

Trevor Fayas answered on January 9, 2020 16:32 (last edited on January 9, 2020 16:33)

The Children i believe will not be a cached call unless it is within a cached method.

If you put the logic in a Repository you can make a "GetChildren(string ParentPath)" or something like this that returns a list of the Child BaseInfo pages, then the caching mechanism will cache the method automatically (default though is to break the cache on ANY change of any page, you may want to add a speicific cache dependency of "node|##SITENAME##|{0}|childnodes" (the {0} will be populated with the first parameter of the method, in this case the parent's path)

Roughly this is what it would look like:

```

public class KenticoChildPagesRepository : IChildPagesRepository, IRepository
{
    public string cultureName { get; set; }
    public bool latestVersionEnabled { get; set; }
    public KenticoChildPagesRepository(string cultureName, bool latestVersionEnabled)
    {
        this.cultureName = cultureName;
        this.latestVersionEnabled = latestVersionEnabled;
    }

    [CacheDependency("node|##SITENAME##|{0}|childnodes")]
    public IEnumerable<TreeNode> GetChildren(string ParentPath, string[] Columns = null)
    {
        var Children = DocumentHelper.GetDocuments()
            .Path(ParentPath, PathTypeEnum.Children)
            .Culture(cultureName)
            .CombineWithDefaultCulture()
            .Published(!latestVersionEnabled)
            .LatestVersion(latestVersionEnabled);
        if(Columns != null && Columns.Length > 0) {
            Units.Columns(Columns);
        } return Children.ToList();
    }
}

```

Some notes that cultureName and latestVersionEnabled are passed as part of the DancingGoat / MVCCaching system for any constructor of type IRepository.

1 votesVote for this answer Mark as a Correct answer

Stuart1 Freeman1 answered on January 10, 2020 00:02

Hi @Trevor Fayas

Thanks for this. It brings up another issue I was just having that you might have some insight on.

I am using dynamic routing and in some of my controller actions I need to checkout the TreeNode Page Type before querying for the view model data. I created a simple TreeNodeRepository with the following method:

public TreeNode GetPageClassNameAndNodeGUID(string nodeAliasPath)
{
    var page = DocumentHelper.GetDocuments()
        .Path(nodeAliasPath, PathTypeEnum.Single)
       .CombineWithAnyCulture()
       .CombineWithDefaultCulture()
       .Columns("ClassName, NodeID, NodeGUID")
       .OnSite(SiteContext.CurrentSiteName)
       .FirstOrDefault();

    return page;
}

The "Get" prefix in the function (I just learned) causes the returned data to be cached. The problem I am having is I am getting an "Object reference not set to an instance of an object." exception at

public string GetClassNameFromPageRuntimeType(Type type)
{
    return mClassNames.GetOrAdd(type, x => ((TreeNode)Activator.CreateInstance(type)).ClassName.ToLowerInvariant());
}

This is in the Infrastructure/ContentItemMetadataProvider.cs file (taken from the Dancing Goat example). The issue seems to be the type is TreeNode. All my other repository methods with custom types are working fine. Thanks for your help!

0 votesVote for this answer Mark as a Correct answer

Trevor Fayas answered on January 10, 2020 00:09

If the return type of a Get____ Method in a IRepository is of type TreeNode, it tries to add a basic CacheDependency of all pages of that type.

You can add your own cache dependency which may bypass this, i say may because i'm trying to recall if that was an enhancement i made to it (that is featured in the MVC Cache module, which is based on the dancing goat system but better).

I know at minimum my MVC Caching module has a check that if there is any specified [CacheDependency("the|dependency|pattern")] then that is the only thing it will use.

Out of curiosity, are you using my Dynamic Routing? Because the DynamicRoute() assembly attribute should allow you to handle your pages by type automatically, trying to figure out why you would need the classname itself.

https://github.com/KenticoDevTrev/DynamicRouting

1 votesVote for this answer Mark as a Correct answer

Stuart1 Freeman1 answered on January 10, 2020 17:09 (last edited on January 10, 2020 17:20)

Hi Trevor,

Last week I read an article you wrote and attempted to get dynamic routing working with your libraries. I was unsuccessful. The slugs in the admin site did not seem to generate and for the one's that did I couldn't seem to get the routing to work. I'm not totally sure what the slugs are doing and I was unsure of how to apply the dynamic routing attributes. Where do they go? the controller actions? the models? Big gap in my understanding. I would love to take another stab at it. Here's my current setup:

I've got a route like this:

routes.MapRoute(
        name: "SomeTopLevelSiteSection",
        url: "{*url}",
        defaults: new { controller = "SomeTopLevelSiteSection", action = "SubPage" }
    );

In the SubPage controller action looks like this:

public ActionResult SubPage()
    {
        var nodeAliasPath = HttpContext.Request.Path.Replace(HttpContext.Request.ApplicationPath, "");

        var page = _treeNodeRepository.GetPageClassNameAndNodeGUID(nodeAliasPath);

        if (page.ClassName == "MyCustom.GenericContent")
        {
            var treeNode = _genericContentRepository.GetGenericContent(page.NodeGUID);
            var vm = GenericContentViewModel.GetViewModel(treeNode);
            return View("_GenericContent", vm);
        }
        else // Page.ClassName == "MyCustom.ContentWithSideNav"
        {
            var treeNode = _contentWithSideNavRepository.GetContentWithSideNavPage(page.NodeGUID);

            var pageVM = GenericContentWithSideNavViewModel.GetViewModel(treeNode);

            foreach (var node in treeNode.Children)
            {
                var menuItem = new MenuItemViewModel { MenuItemText = node.NodeName, MenuItemRelativeUrl = node.NodeAliasPath };
                pageVM.SideNavMenuItems.Add(menuItem);
            }

            return View("_ContentWithSideNav", pageVM);
        }
    }

That's a shortened version of the action as there are more types. How can I eliminate this type check?? My site's hierarchy has nodes 4-5 levels deep with varying page types throughout. As always, thanks for your help!

0 votesVote for this answer Mark as a Correct answer

Trevor Fayas answered on January 10, 2020 18:21

The Url slugs compile each page's "Url" based on the Url Pattern of the Page Type. {% NodeAliasPath %} is often the safest route as they are always unique on the node.

Then, if you read the documentation on the github, you link Page Type(s) to either Controllers, Controller + Actions, or just Views using Assembly attributes, the same way you do with Widgets and Page Templates.

It routes things automatically based on these attributes.

Read the readme here.

If your URL Slugs won't generate, it's probably becuase you have 2 pages that are generating the same URL Slug, which will cause it to stop. You can go to the Dynamic Routing module within the Mother and see what the errors are.

1 votesVote for this answer Mark as a Correct answer

Stuart1 Freeman1 answered on January 10, 2020 20:56

Hi Trevor,

I will give it a try again. being new to Kentico, I'm not sure how Assembly attributes are used with Widgets or Page Templates either. I'm guessing there will be an example in your boilerplate examples branch. I'll take a look there. Thanks for all the info!

0 votesVote for this answer Mark as a Correct answer

Stuart1 Freeman1 answered on January 13, 2020 15:21

Hi Trevor, taking another look at your boilerplate. You seem to be intercepting requests, grabbing a treenode based on the nodealiaspath in order to check the type and route the request? https://github.com/KenticoDevTrev/KenticoBoilerplate_v12/blob/master/Boilerplate/Library/Dynamic%20Route%20Handling/KMVCDynamicHttpHandler.cs line 64? Is this query cached?

0 votesVote for this answer Mark as a Correct answer

   Please, sign in to be able to submit a new answer.