Custom fields always empty with DocumentHelper.GetDocuments() when returning multiple types

Stuart1 Freeman1 asked on December 18, 2019 23:28

Hi All,

I am trying to use the DocumentHelper to query a document path that contains multiple types. I would like to be able to access the custom fields in those nodes/documents but I am unable to. When I attempt to GetValue("MyCustomField") the result is always empty. I can see them in the node object but they are always empty.

I have tried the following queries with the same result:

DocumentHelper.GetDocuments()
    .Type("MyCustom.Menu", q => q.Default())
    .Type("MyCustom.MenuItem", q => q.Default())
    .WithCoupledColumns()
    .OrderBy("NodeOrder")
    .Path("/My-AAA-menu")
    .Published(true);

and

DocumentHelper.GetDocuments()
    .Types("MyCustom.Menu", "MyCustom.MenuItem")
    .WithCoupledColumns()
    .OrderBy("NodeOrder")
    .Path("/My-AAA-menu")
    .Published(true);             

Any help is appreciated. Thanks!

Correct Answer

Brenden Kehren answered on December 19, 2019 17:42

Read this article more in depth. About half way down it says "MultiDocument Query" with a few examples.

"By default, the MultiDocumentQuery selects type-specific columns (coupled columns) only if the type query is parameterized"

// Get news title from /News section and article title from /Community section together with document name column
    var documents = DocumentHelper.GetDocuments()
                                  .Type("CMS.News", q => q
                                      .Columns("NewsTitle")
                                      .Path("/News", PathTypeEnum.Children))
                                  .Type("CMS.SimpleArticle", q => q
                                      .Columns("ArticleTitle")
                                      .Path("/Community", PathTypeEnum.Children))
                                  .Columns("DocumentName")
                                  .OnSite("CorporateSite");
2 votesVote for this answer Unmark Correct answer

Recent Answers


Brenden Kehren answered on December 19, 2019 06:33

Try modifying your path to be /My-OPL-menu/% or use the second parameter on the Path() property and set to child pages.

0 votesVote for this answer Mark as a Correct answer

Stuart1 Freeman1 answered on December 19, 2019 16:33 (last edited on December 19, 2019 22:23)

Hi Brenden, thanks for the quick reply.

I've tried what you've said:

DocumentHelper.GetDocuments()
                .Types("AAACustom.Menu", "AAACustom.MenuItem")
                .Path("/My-AAA-menu", PathTypeEnum.Children)
                .WithCoupledColumns()
                .OrderBy("NodeOrder")                
                .Published(true);

Similar result. The difference is I'm no longer getting the parent document in the query. I have multiple pages using the parent type but in this query I specifically need the particular parent node and then all the children which are of menu and menuItem type (custom types, not the built in CMS ones).

The query results have the custom fields in them but they are empty strings. Any ideas? Thanks again.

** EDIT ** For clarity sake: the query results return all the nodes I need and inside a "MenuItem" node for instance I have custom fields like "MenuItemText", "MenuItemRelativeUrl", "MenuItemIconUrl". When I am debugging and inspect one of the MenuItem nodes I can see the above fields/properties in the root of the node and all of their values are empty strings "". When I try menuItemNode.GetValue("MenuItemText") i get an exception because there is no value available.

0 votesVote for this answer Mark as a Correct answer

Peter Mogilnitski answered on December 19, 2019 17:20

You have to add OnSite. i.e.

DocumentHelper.GetDocuments()
                .Types("OplCustom.Menu", "OplCustom.MenuItem")
                .Path("/My-OPL-menu", PathTypeEnum.Children)
                .WithCoupledColumns()
                .OnSite(SiteContext.CurrentSiteName)
                .OrderBy("NodeOrder")                
                .Published(true);
1 votesVote for this answer Mark as a Correct answer

Brenden Kehren answered on December 19, 2019 17:31

If you're getting multiple page types and getting an error on a given field that exists in one page type but not another, then you need to perform some checking to see what page type record you're looking at. Something like this:

var docs = DocumentHelper.GetDocuments()
                .Types("OplCustom.Menu", "OplCustom.MenuItem")
                .Path("/My-OPL-menu", PathTypeEnum.Children)
                .WithCoupledColumns()
                .OrderBy("NodeOrder")                
                .Published(true);


foreach (var d in docs)
{
    string name = "";
    switch (d.ClassName)
    {
        case "OplCustom.Menu":
            name = d.GetValue("MyCustomField");
        break;
        case "CMS.MenuItem":
            name = d.GetValue("MenuItemName");
        break;
    }
}
1 votesVote for this answer Mark as a Correct answer

Stuart1 Freeman1 answered on December 19, 2019 17:32

Hi Peter,

That query did populate the parent node's "Menu" type custom fields. The children "MenuItem" custom types fields remain empty. Any other ideas?

Side question: Is this type of query something (you) other kentico developers find themselves doing? It seems like it would be a common type of query to be done.

0 votesVote for this answer Mark as a Correct answer

Stuart1 Freeman1 answered on December 19, 2019 17:34 (last edited on December 19, 2019 17:46)

Hi Brenden, this is exactly what I'm doing. And the custom fields for all child types are there in the node object but are empty even though they absolutely have values in the CMS.

** Edit **

One difference I have is if I try and name = d.GetValue("MyCustomField") I get an error in the IDE -> 'cannot implicitly convert type 'object' to string'. The type of d is TreeNode. Is that correct? I'm having to name = d.GetValue("MyCustomField").ToString() but of course my values are always empty. Just wondering if my namespacing/types are incorrect?

0 votesVote for this answer Mark as a Correct answer

Peter Mogilnitski answered on December 19, 2019 17:44

Similar topic was discussed here The multiple type query should be something like this:

var queryText = DocumentHelper.GetDocuments()
    .Type("CMS.MenuItem", q => q
        .Columns("DocumentName", "NodeAliasPath")
        .Path("/Community/Events/", PathTypeEnum.Section))
    .Type("CMS.BookingEvent", q => q
        .Columns("DocumentName", "NodeAliasPath")
        .Path("/Community/Events/", PathTypeEnum.Section))
    .OnSite("CorporateSite")
    .Columns("DocumentName", "NodeAliasPath")

Hope this helps.

1 votesVote for this answer Mark as a Correct answer

Stuart1 Freeman1 answered on December 19, 2019 17:59

Hi Brenden,

In the article you've referenced it says:

"If you want to include the coupled columns in the results and you don’t need to parameterize the type query, simply use:

// Get news items from /News section including type specific columns
var documents = DocumentHelper.GetDocuments()
                            .Type("CMS.News")
                            .Path("/News", PathTypeEnum.Children)
                            .OnSite("CorporateSite")
                            .WithCoupledColumns();

It would appear as though .WithCoupledColumns should include the custom fields/columns?

0 votesVote for this answer Mark as a Correct answer

Brenden Kehren answered on December 19, 2019 18:04

Correct, that example shows only 1 page type though.

1 votesVote for this answer Mark as a Correct answer

Stuart1 Freeman1 answered on December 19, 2019 18:36 (last edited on December 19, 2019 22:22)

Ok I've got this now:

DocumentHelper.GetDocuments()
                .Type("AAACustom.Menu", q=> q
                    .Columns("TopMenuID", "MenuName")) 
                .Type("AAACustom.MenuItem", q => q
                    .Columns("MenuItemID", "MenuItemText", "MenuItemPage", "MenuItemIcon", "ExternalUrl"))
                .Path("/My-AAA-menu")
                .WithCoupledColumns()
                .OnSite(SiteContext.CurrentSiteName)
                .PublishedVersion().Published();

With the same result. All blank/null when I childNode.GetValue("MenuItemText", "").ToString(). I'm using Kentico 12, latest. Thanks for all your help. Any ideas? Do you know where in the node object itself the custom fields would be located?

** EDIT ***

I am totally new to Kentico. What I am getting back from this query is always a single document which is a Menu document. What I am doing is looping through the Children of this document and these are of type TreeNode.

Looks like:

foreach (TreeNode childNode in menuTreeNode.Children)

Could it be the TreeNode children are cannot store these custom fields? Am I missing something here? Thanks!

0 votesVote for this answer Mark as a Correct answer

Stuart1 Freeman1 answered on December 19, 2019 20:19 (last edited on December 19, 2019 20:19)

I've discovered my issue. I made the assumption the DocumentQuery results were being returned hierarchically rather than a 1 dimensional collection. And so I created a recursive function that was traversing through a single root document and it's children.

Menu
    (Children)
    - Menu
        (Children)
        - MenuItem
        - MenuItem
    - Menu
        (Children)
        - MenuItem
        - MenuItem
    - MenuItem
    - MenuItem

The root document in this case which is a menu does have the custom fields. Any document in .Children at any level does not have the custom fields.

This seems unintuitive. In the case of nested content like this menu (or example OrderHistory->Orders ->OrderItems, TaxItems) I have to query everything and then rebuild the Tree structure.

Maybe I am missing something? Anyway clever way to either query and force the documents under .Children to include custom fields? or clever way to rebuild the tree structure with the correct order??

Thanks!

0 votesVote for this answer Mark as a Correct answer

Brenden Kehren answered on December 19, 2019 20:25

Do an order by set to NodeLevel, NodeOrder, NodeName. Should get it exactly like it is in the content tree.

2 votesVote for this answer Mark as a Correct answer

Stuart1 Freeman1 answered on December 19, 2019 22:22 (last edited on December 19, 2019 22:25)

Hi Brenden, I've finally got it working thanks to your and Peter's help. This -> .Children.WithAllData when looping through the nodes was key.

In my Controller:

    public ActionResult GetMyAAAMenu()
    {
        var menuTreeNode = MenuProvider.GetMenuTreeNodes().FirstOrDefault();

        var model = new MenuViewModel();

        if (menuTreeNode.NodeHasChildren)
        {
            foreach (TreeNode childNode in menuTreeNode.Children.WithAllData)
            {
                if (childNode.ClassName == "AAACustom.Menu")
                {
                    if (model.SubMenus == null)
                        model.SubMenus = new List<MenuViewModel>();
                    model.SubMenus.Add(GetMenuTree(childNode));
                }
                else
                {
                    var menuItem = new Models.MenuItemViewModel
                    {
                        MenuItemText = childNode.GetValue<string>("MenuItemText", ""),
                        MenuItemRelativeUrl = childNode.GetValue<string>("MenuItemRelativeUrl", "").ToString(),
                        MenuItemIconUrl = childNode.GetValue<string>("MenuItemIconUrl", "").ToString(),
                        ExternalUrl = childNode.GetValue<string>("ExternalUrl", "").ToString()
                    };
                    if (model.MenuItems == null)
                        model.MenuItems = new List<Models.MenuItemViewModel>();
                    model.MenuItems.Add(menuItem);
                }
            }
        }
        return PartialView("_MyAAAMenu", model);
    }

    protected MenuViewModel GetMenuTree(TreeNode menuNode)
    {
        var menuViewModel = new MenuViewModel();

        menuViewModel.MenuName = menuNode.GetValue("MenuName", "").ToString();
        if (menuNode.NodeHasChildren)
        {
            foreach (TreeNode childNode in menuNode.Children.WithAllData)
            {
                if (childNode.ClassName == "AAACustom.Menu")
                {
                    menuViewModel.SubMenus.Add(GetMenuTree(childNode));
                }
                else // ClassName = AAACustom.MenuItem
                {
                    var menuItem = new AAA.Models.MenuItemViewModel
                    {
                        MenuItemText = childNode.GetValue("MenuItemText", "").ToString(),
                        MenuItemRelativeUrl = childNode.GetValue("MenuItemRelativeUrl", "").ToString(),
                        MenuItemIconUrl = childNode.GetValue("MenuItemIconUrl", "").ToString(),
                        ExternalUrl = childNode.GetValue("ExternalUrl", "").ToString()
                    };
                    if (menuViewModel.MenuItems == null)
                        menuViewModel.MenuItems = new List<Models.MenuItemViewModel>();
                    menuViewModel.MenuItems.Add(menuItem);
                }
            }
        }
        return menuViewModel;
    }

And in my provider:

    public static MultiDocumentQuery GetMenuTreeNodes()
    {
        return DocumentHelper.GetDocuments()
            .Type("AAACustom.Menu", q => q
                .Columns("TopMenuItemsID", "MenuName"))
            .Type("AAACustom.MenuItem", q => q
                .Columns("MenuItemID", "MenuItemText", "MenuItemPage", "MenuItemIcon", "ExternalUrl"))
            .OnSite(SiteContext.CurrentSiteName)
            .Path("/My-AAA-menu")
            .OrderBy("NodeLevel", "NodeOrder", "NodeName")
            .Published();
    }

I'm sure there are other ways of doing this but thanks for helping me get to this point.

0 votesVote for this answer Mark as a Correct answer

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