leekelleher/umbraco-contentment

Data List: Custom Data source accessing the querystring ID

SvenGoormans opened this issue ยท 18 comments

Note: for support questions, please use the Umbraco Community forum. This repository's issues are reserved for bug reports and feature requests.
Hello !

We're using contentment package to replace the NuPicker from Umbraco V7.
We have several custom property editors with each their own custom data source.

The problem we're having is that in our GetItems method we can't reach the Id of the node that we currently are loading in the backoffice. The request url is always '/umbraco/backoffice/UmbracoApi/Content/GetEmpty?contentTypeAlias=theAlias&parentId=-20'.

What is the current behavior?

HttpContext.Current.Request.Querystring["Id"] = null

What is the expected behavior?

HttpContext.Current.Request.Querystring["Id"] => Should be the Id of the node we're loading in the backoffice.

What is the motivation / use case for changing the behavior?

We have different homepages for each culture. In each homepage we have a configuration node with a list of items. Those items are used as custom data source. Without the Id of the node where we need the items, we are unable to know which homepage we need to get the items from.

Please tell us about your set-up:

  • Contentment version: [1.0.0]
  • Umbraco version: [8.6.1]
  • Browser (if applicable?): [all]

Anything else?

Hi @SvenGoormans,

For the Umbraco Content data source (in upcoming v1.1), I'm using the UmbracoContextAccessor to access the querystring via HttpContext. I'm not sure if that makes a difference from using HttpContext.Current directly?

https://github.com/leekelleher/umbraco-contentment/blob/dev/v1.1/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoContentDataListSource.cs#L60-L67

Hey, thanks for your quick response !

Unfortunately it doesn't make a difference from using httpContext.Current directly.

The weird thing is, we've made several custom property editors with each a custom data source.
In some of the custom property editors it is possible to get the node Id by using HttpContext.Current.Request.QueryString["id"] when the request url is 'getById' instead of 'GetEmpty'.

Important note: we're trying to use the custom property editor in a element type so we can use it as nested content.

Would falling back on the "parentId" querystring param work?

The parentId querystring is always -20. So no that is unfortunately no option.

๐Ÿค” Unfortunately, there isn't anything else on the querystring that gives the context of the container node, so there's nothing the data source code could use to get the container node's ID.

This will be the same problem I'll hit with developing my Umbraco Content data source (for v1.1). Of which, I don't currently have a workaround solution for it.

The fundamental difference with nuPickers and Contentment's Data List, is that nuPickers sends an AJAX request back to the server to get the data source items - which contains extra meta-data about the node and property. Whereas with Contentment's Data List the data source items are gathered during the initial payload, (without the extra AJAX request), but no contextual meta-data. (This was a design decision - I never intended to reproduce nuPickers exactly.)

In terms of nuPickers, I'm aware that there's an effort to make it compatible with v8, details here: uComponents/nuPickers#199

Ok, still weird that in some cases the querystring contains the Id, and in this case, used as nested content the querystring Id is always null with parentId -20.

For now we'll find another solution. Thanks for your response !

The "id" querystring should be consistently null (and parentId=-20) when using within Nested Content, as when the node's scaffold is requested, see here, nestedcontent.controller.js#L430, that's where -20 is hardcoded.

I encountered similar whilst developing Content Blocks (again for upcoming v1.1), where I try to figure out the container's node ID and send that through instead, see here, content-blocks.overlay.js#L154.

So it's possible that this could be resolved within Nested Content, (by replacing the -20 with the current node ID - using the editorState - like I do in Content Blocks), but I'm unsure what wider impact in doing that may have.


I'll close this ticket off, as there currently isn't a viable workaround solution within Contentment itself.

Hi Lee, I just wanted to let you know that there is a viable workaround solution within Contentment itself.

We experienced the same problem today where the Umbraco Content data source was not working when used in Nested Content and using xPath.

The workaround was to create a Custom Data List Source like this:

using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Xml;
using Umbraco.Web;
using UmbConstants = Umbraco.Core.Constants;
using Umbraco.Community.Contentment.DataEditors;

namespace MyProject.Web.App_Code
{
    public class PeopleDataListSource : IDataListSource
    {
        private readonly IUmbracoContextAccessor _umbracoContextAccessor;

        public PeopleDataListSource(IUmbracoContextAccessor umbracoContextAccessor)
        {
            _umbracoContextAccessor = umbracoContextAccessor;
        }

        public string Name => "Author Content Items";

        public string Description => "Use authors content items as a data source.";

        public string Icon => "icon-users";

        public OverlaySize OverlaySize => OverlaySize.Small;

        public Dictionary<string, object> DefaultValues => new Dictionary<string, object>();

        public IEnumerable<ConfigurationField> Fields => Enumerable.Empty<ConfigurationField>();


        public IEnumerable<DataListItem> GetItems(Dictionary<string, object> config)
        {
            var umbracoContext = _umbracoContextAccessor.UmbracoContext;

            List<DataListItem> results = new List<DataListItem>();

            //get call content items which are using the document type alias of 'person'
            var people = umbracoContext.Content.GetByXPath(false, "//person");

            //make sure there are some people items
            if (people != null && people.Any())
            {

                //loop through the people itmes
                foreach (var person in people)
                {

                    //generate a udi from the key property of the content item
                    //we will use this to store as the value of the author picker
                    var udi = Udi.Create(Constants.UdiEntityType.Document, person.Key);
                    if (udi == null) break;

                    //create a new DataListItem object to store the data
                    var item = new DataListItem()
                    {
                        Name = person.Name,
                        Value = udi.ToString()
                    };

                    //check if the person record has a photo
                    if (person.HasValue("photo"))
                    {
                        var photo = person.Value<IPublishedContent>("photo");
                        item.Icon = photo.GetCropUrl(120, 120);
                    }

                    //add the item to our list of results
                    results.Add(item);
                }

                return results;
            }

            return Enumerable.Empty<DataListItem>();
        }

    }
}

So instead of using Umbraco Content and entering the xPath we can just use this Custom Data List Source and it works how it was intended to.

I came across this as an issue today while trying to get a "context-aware" Custom Data Source to work inside a BlockList Editor. The url that comes through to the data source code is "/umbraco/backoffice/UmbracoApi/Content/GetEmptyByKeys", so no query string to work with at all.

What's interesting is that the XPath in the MultiNodeTreePicker, when used in Block List Editor DOES work, so it must be getting passed through somehow...

I was puzzling over this one again today. I'd love to be able to get the current content node when using the DataList in a Block editor...

And I believe I've figured out a solution - Using session variables!

By adding an EditorModelNotification (aka: Event that runs on content node editor load), which stores the current Node Id in a session variable, The custom IDataListSource can retrieve the NodeId from the session:

public class MyCustomDataSource : IDataListSource
{
	
	...
	
	private readonly IRequestAccessor _requestAccessor;
	private readonly IHttpContextAccessor _httpContextAccessor;

	public MyCustomDataSource (
		IHttpContextAccessor httpContextAccessor,
		IRequestAccessor requestAccessor,
		)
	{
		
		_httpContextAccessor = httpContextAccessor;
		_requestAccessor = requestAccessor;
	}
	
	public IEnumerable<DataListItem> GetItems(Dictionary<string, object> config)
	{
		var items = new List<DataListItem>();
		var fileName = config["ConfigFileName"].ToString();

		var currentNode = GetCurrentNodeId(_requestAccessor, _httpContextAccessor);

		//If no node Id - skip all this
		if (currentNode == 0)
		{
			return Enumerable.Empty<DataListItem>();
		}
		else
		{
			//Do stuff with current node Id to fill items List...
			items = ....
		}
		
		return items;
	}
	
	private int GetCurrentNodeId(IRequestAccessor RequestAccessor, IHttpContextAccessor HttpContextAccessor)
	{
		var nodeContextId = 0;

		// NOTE: First we check for "id" (if on a content page), then "parentId" (if editing an element).
		var hasId = int.TryParse(RequestAccessor.GetQueryStringValue("id"), out var currentId);
		if (hasId)
		{
			nodeContextId = currentId;
		}
		else
		{
			var hasParentId = int.TryParse(RequestAccessor.GetQueryStringValue("parentId"), out var parentId);
			if (hasParentId)
			{
				nodeContextId = parentId;
			}
			else
			{
				//Look in the session
				var umbracoNodeObject = HttpContextAccessor.HttpContext.Session.GetObjectFromJson<UmbracoSessionInfo>("UmbracoNode");
				if (umbracoNodeObject != null)
				{
					nodeContextId = umbracoNodeObject.NodeId;
				}
			}
		}

		//If no node found, something is wrong....
		if (nodeContextId == 0)
		{
			return 0;
		}
		else
		{
			return (int)nodeContextId;
		}
	}
		
}

Perhaps you could integrate this into Contentment itself, so things would "just work", but if you don't want to do that, developers who would like to have access to the current node can use this gist as reference for storing the Node Id in session.

Hello @hfloyd, cool solution!
This seems to be only working for Umbraco v9+ with .Net Core.
Any idea how we could use this code in an umbraco v8 .net project?

Hi @Sven883 I haven't tried it in Umbraco 8, my suggestion would be to figure out what part isn't working and look for the .Net framework equivalent code. Sometimes it's just namespace changes, etc. Also a little research on Session variables in .Net Framework should sort it out. Also, look at the "Event" equivalent - https://our.umbraco.com/Documentation/Reference/Events/EditorModel-Events/

I was puzzling over this one again today. I'd love to be able to get the current content node when using the DataList in a Block editor...

And I believe I've figured out a solution - Using session variables!

By adding an EditorModelNotification (aka: Event that runs on content node editor load), which stores the current Node Id in a session variable, The custom IDataListSource can retrieve the NodeId from the session:

public class MyCustomDataSource : IDataListSource
{
	
	...
	
	private readonly IRequestAccessor _requestAccessor;
	private readonly IHttpContextAccessor _httpContextAccessor;

	public MyCustomDataSource (
		IHttpContextAccessor httpContextAccessor,
		IRequestAccessor requestAccessor,
		)
	{
		
		_httpContextAccessor = httpContextAccessor;
		_requestAccessor = requestAccessor;
	}
	
	public IEnumerable<DataListItem> GetItems(Dictionary<string, object> config)
	{
		var items = new List<DataListItem>();
		var fileName = config["ConfigFileName"].ToString();

		var currentNode = GetCurrentNodeId(_requestAccessor, _httpContextAccessor);

		//If no node Id - skip all this
		if (currentNode == 0)
		{
			return Enumerable.Empty<DataListItem>();
		}
		else
		{
			//Do stuff with current node Id to fill items List...
			items = ....
		}
		
		return items;
	}
	
	private int GetCurrentNodeId(IRequestAccessor RequestAccessor, IHttpContextAccessor HttpContextAccessor)
	{
		var nodeContextId = 0;

		// NOTE: First we check for "id" (if on a content page), then "parentId" (if editing an element).
		var hasId = int.TryParse(RequestAccessor.GetQueryStringValue("id"), out var currentId);
		if (hasId)
		{
			nodeContextId = currentId;
		}
		else
		{
			var hasParentId = int.TryParse(RequestAccessor.GetQueryStringValue("parentId"), out var parentId);
			if (hasParentId)
			{
				nodeContextId = parentId;
			}
			else
			{
				//Look in the session
				var umbracoNodeObject = HttpContextAccessor.HttpContext.Session.GetObjectFromJson<UmbracoSessionInfo>("UmbracoNode");
				if (umbracoNodeObject != null)
				{
					nodeContextId = umbracoNodeObject.NodeId;
				}
			}
		}

		//If no node found, something is wrong....
		if (nodeContextId == 0)
		{
			return 0;
		}
		else
		{
			return (int)nodeContextId;
		}
	}
		
}

Perhaps you could integrate this into Contentment itself, so things would "just work", but if you don't want to do that, developers who would like to have access to the current node can use this gist as reference for storing the Node Id in session.

Thanks a lot for this workaround @hfloyd. Ran into the same issue and saved me a lot of time. Worked straight out of the bag ๐Ÿ‘

+1 H5YR! Thanks a lot for this workaround @hfloyd, worked perfectly.

My site is in U8 so I made the required adaptions, mostly around the types used. Use EditorModelEventManager.SendingContentModel in your component/composition to get the v8 equivalent of SendingContentNotification (only key difference is e.Model instead of notification.Content) and in SessionExtensions use HttpSessionState instead of ISession. Implement GetString and SetString as simple NameValueCollection references (session[key])

Follow up on this issue for the Block List editor - a patch to resolve this has been merged in, umbraco/Umbraco-CMS#15063, due for release in Umbraco v13.1.0.

@leekelleher I was looking at this again for a v13 site - wondering if I could leave out my "save the nodeId to session" code... but can't figure out how your fix is accessible in the IDataListSource code...

I'm still seeing the HttpContextReques available as Path = "/umbraco/backoffice/umbracoapi/content/GetEmptyByKeys" with an empty QueryString. What am I missing here?

@hfloyd do you mean for the Block List editor request? If so, it's because the GetEmptyByKeys request sends as a POST, and not only that be the body of the request is a JSON object. I managed to figure out a solution in PR #389 (for upcoming Contentment v5 for Umbraco 13, but I didn't backport it for current Contentment v4, it's a lot to support that scenario). Full story on that PR.

Ah, thanks for the explanation @leekelleher! I guess I'll just use the session var until Contentment v5, then :-)