Try our conversational search powered by Generative AI!

Vladimir Vedeneev
Dec 5, 2010
  3171
(3 votes)

Workaround for several nodes with the same ID in the editor’s tree

Have you ever tried to display the same node in the several places simultaneously in the editor tree?

For example, returning a same child PageReference for different parents will result in it. I believe you faced with it during debugging your own page provider.

If not, first I’ll show you

How it NOT works

First of all, believe me that Structure tab and Favorites tab uses the same tree control. They really do. Ok, you can use reflector and check it.

Create simple hierarchy like this one:

Parent -> Chid1 -> Child2

Right-click on each of the nodes and select “Add to favorites”. You should see the following in the Favorites tab:

 Favorites

Expand the first level nodes, and you will see two instances of the same node in the tree:

Several same nodes in the tree 

But EPiServer isn’t ready for such cases. There’s no tooltip for second Child2 node. Click on the second Child2 node – selected is first Child2. Try to expand second Child1 node:

Several same nodes in the tree

Nothing happens!  Ok, try to “collapse” it and expand it again:

Several same nodes in the tree

And you can see the whole new tree under the second Child1 node!

Why is the world so cruel?

If not digging too deep, the idea is simple: they use the PageReference to generate ID of a node control, so from two identical nodes they got two identical HTML controls with the same ID… Which breaks their JS.

Workaround?

My first idea was to generate random IDs for all the nodes; for example, use Guid.NewGuid(). I’ve tried it. Now I know. The tree uses node ID to retrieve node PageReference. Yes, a node has PageReference among other properties on the client. BUT a tree prefer to use node ID.

Ok, I couldn’t sleep for weeks and found an Idea – all I need is something that is parsed into the same PageRefernces, but differs in string representation. I tried a number of ideas here, but most simple is just adding leading zero to a string representation of a PageReference. For the integer it doesn’t change its numerical meaning, so PageReferences like “1” and “01” are the same, but these ARE different client IDs for controls! This will also work for complex references, like 1__RemoteSite. Exactly what I need.

So, basically all I need is to append zeroes to IDs of the “duplicated” nodes, which are already shown in the tree under some other paths. Bad thing here is that there’s no (easy?) way to understand what nodes are already shown to the particular visitor, so I need to create some cache on the server side.

Implementation

It is easy: create & register adapter for tree, subscribe for ItemDataBound of TreeControl, and change DataPath in a smart way.

Control adapter registration (file <Web project>\App_Browsers\AdapterMappings.browser):

   1: <browsers>
   2:   <browser refID="Default">
   3:     <controlAdapters>
   4:       <adapter controlType="EPiServer.UI.WebControls.PageTreeView" adapterType="MyNamespace.PageTreeViewAdapter" />
   5:     </controlAdapters>
   6:   </browser>
   7: </browsers>

Control adapter implementation sample (simple, to understand idea):

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Reflection;
   5: using System.Text;
   6: using System.Web.UI.WebControls.Adapters;
   7:  
   8: using EPiServer.UI.WebControls;
   9:  
  10: namespace MyNamespace
  11: {
  12:     public class PageTreeViewAdapter : HierarchicalDataBoundControlAdapter
  13:     {
  14:         protected override void OnInit(EventArgs e)
  15:         {
  16:             base.OnInit(e);
  17:             ((PageTreeView)this.Control).PageTreeViewItemDataBound += this.TreeControl_PageTreeViewItemDataBound;
  18:         }
  19:  
  20:         private static int dummyCounter;
  21:  
  22:         private void TreeControl_PageTreeViewItemDataBound(object sender, PageTreeViewEventArgs e)
  23:         {
  24:             // Generate a number of leading zeroes
  25:             var leadingZeroes = new StringBuilder().Append('0', dummyCounter++);
  26:  
  27:             // We cannot just rewrite DataPath - use reflection
  28:             var property = typeof(PageTreeNode).GetProperty("DataPath", BindingFlags.Public | BindingFlags.Instance);
  29:  
  30:             property.SetValue(e.Item, leadingZeroes + e.Item.DataPath, null);
  31:         }
  32:     }
  33: }

Control adapter implementation sample (more smart):

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Reflection;
   5: using System.Web.UI.WebControls.Adapters;
   6:  
   7: using EPiServer.UI.WebControls;
   8:  
   9: namespace MyNamespace
  10: {
  11:     public class PageTreeViewAdapter : HierarchicalDataBoundControlAdapter
  12:     {
  13:         private const BindingFlags PropertyBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy;
  14:         private static readonly PropertyInfo PageTreeNodeDataPathProperty = typeof(PageTreeNode).GetProperty("DataPath", PropertyBindingFlags);
  15:         private static readonly Dictionary<string, Dictionary<string, string>> TreeToDataPathsHash = new Dictionary<string, Dictionary<string, string>>();
  16:  
  17:         protected PageTreeView TreeControl
  18:         {
  19:             get { return Control as PageTreeView; }
  20:         }
  21:  
  22:         protected override void OnInit(EventArgs e)
  23:         {
  24:             base.OnInit(e);
  25:             this.TreeControl.PageTreeViewItemDataBound += this.TreeControl_PageTreeViewItemDataBound;
  26:         }
  27:  
  28:         private void TreeControl_PageTreeViewItemDataBound(object sender, PageTreeViewEventArgs e)
  29:         {
  30:             var newDataPath = this.GetDataPath(e.Item.DataPath, e.Item.CombinedDataPath);
  31:             if (newDataPath != null)
  32:             {
  33:                 PageTreeNodeDataPathProperty.SetValue(e.Item, newDataPath, null);
  34:             }
  35:         }
  36:  
  37:         private string GetDataPath(string nodeId, string nodeViewPath)
  38:         {
  39:             // The same path CAN appear in different trees, so use separate cache for each tree (Structure, Favorites, select Page dialog etc)
  40:             string treeKey = this.TreeControl.ClientID;
  41:             Dictionary<string, string> idToPathHash;
  42:             if (!TreeToDataPathsHash.TryGetValue(treeKey, out idToPathHash))
  43:             {
  44:                 idToPathHash = new Dictionary<string, string>();
  45:                 TreeToDataPathsHash.Add(treeKey, idToPathHash);
  46:             }
  47:  
  48:             var index = nodeViewPath.LastIndexOf('/');
  49:             var parentPath = nodeViewPath.Substring(0, index);
  50:  
  51:             nodeId = nodeId.TrimStart('0');
  52:  
  53:             for (;;)
  54:             {
  55:                 string existingParentPath;
  56:                 if (idToPathHash.TryGetValue(nodeId, out existingParentPath))
  57:                 {
  58:                     if (string.Compare(existingParentPath, parentPath, StringComparison.InvariantCultureIgnoreCase) == 0)
  59:                     {
  60:                         return nodeId;
  61:                     }
  62:                 }
  63:                 else
  64:                 {
  65:                     idToPathHash.Add(nodeId, parentPath);
  66:                     return nodeId;
  67:                 }
  68:                 nodeId = "0" + nodeId;
  69:             }
  70:         }
  71:     }
  72: }

It works!

That’s it:

image

Ideas? Comments? Am I doing something absolutely wrong?

Dec 05, 2010

Comments

Joel Abrahamsson
Joel Abrahamsson Dec 6, 2010 10:58 AM

Very clever solution Vladimir! And a very interesting read.

Have you filed a bug with EPiServer?

Vladimir Vedeneev
Vladimir Vedeneev Dec 6, 2010 09:23 PM

Thanks! Yep, they confirmed it's a bug, but it's not public yet.

Joel Abrahamsson
Joel Abrahamsson Dec 17, 2010 12:12 AM

I tried in R2 and it has been fixed for the favorites tab. Unfortunately though the problem remains in the structure tab :(

Joel Abrahamsson
Joel Abrahamsson Dec 17, 2010 12:13 AM

Good part however is that your fix still works :)

Please login to comment.
Latest blogs
Visitor Groups Usage Report For Optimizely CMS 12

This add-on offers detailed information on how visitor groups are used and how effective they are within Optimizely CMS. Editors can monitor and...

Adnan Zameer | Apr 18, 2024 | Syndicated blog

Azure AI Language – Abstractive Summarisation in Optimizely CMS

In this article, I show how the abstraction summarisation feature provided by the Azure AI Language platform, can be used within Optimizely CMS to...

Anil Patel | Apr 18, 2024 | Syndicated blog

Fix your Search & Navigation (Find) indexing job, please

Once upon a time, a colleague asked me to look into a customer database with weird spikes in database log usage. (You might start to wonder why I a...

Quan Mai | Apr 17, 2024 | Syndicated blog

The A/A Test: What You Need to Know

Sure, we all know what an A/B test can do. But what is an A/A test? How is it different? With an A/B test, we know that we can take a webpage (our...

Lindsey Rogers | Apr 15, 2024

.Net Core Timezone ID's Windows vs Linux

Hey all, First post here and I would like to talk about Timezone ID's and How Windows and Linux systems use different IDs. We currently run a .NET...

sheider | Apr 15, 2024

What's new in Language Manager 5.3.0

In Language Manager (LM) version 5.2.0, we added an option in appsettings.json called TranslateOrCopyContentAreaChildrenBlockForTypes . It does...

Quoc Anh Nguyen | Apr 15, 2024