<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://marioarce.github.io/marioarce/feed.xml" rel="self" type="application/atom+xml" /><link href="https://marioarce.github.io/marioarce/" rel="alternate" type="text/html" /><updated>2026-07-03T15:22:57+00:00</updated><id>https://marioarce.github.io/marioarce/feed.xml</id><title type="html">Mario Alberto Arce</title><subtitle>AWS Cloud Architect &amp; Software Engineer | 20+ years building serverless,  event-driven systems | Founder of PowerCSharp | Sharing architectural patterns,  production-grade code, and deep technical expertise.</subtitle><author><name>Mario Alberto Arce</name></author><entry><title type="html">Extending Sitecore TreeList with Queryable Data Sources and Validation</title><link href="https://marioarce.github.io/marioarce/sitecore/csharp/custom-controls/2026/06/30/extending-sitecore-treelist-queryable-data-sources.html" rel="alternate" type="text/html" title="Extending Sitecore TreeList with Queryable Data Sources and Validation" /><published>2026-06-30T17:00:00+00:00</published><updated>2026-06-30T17:00:00+00:00</updated><id>https://marioarce.github.io/marioarce/sitecore/csharp/custom-controls/2026/06/30/extending-sitecore-treelist-queryable-data-sources</id><content type="html" xml:base="https://marioarce.github.io/marioarce/sitecore/csharp/custom-controls/2026/06/30/extending-sitecore-treelist-queryable-data-sources.html"><![CDATA[<h1 id="extending-sitecore-treelist-with-queryable-data-sources-and-validation">Extending Sitecore TreeList with Queryable Data Sources and Validation</h1>

<p>So here’s the thing about Sitecore’s out-of-the-box TreeList fields—they’re great until they’re not. And on this one enterprise project, they definitely weren’t cutting it.</p>

<p>I needed dynamic queries, template filtering, and validation warnings. The kind of stuff that makes content authors’ lives easier and prevents 3am “why is this broken?” Slack messages.</p>

<p><br /></p>

<h2 id="standing-on-shoulders">Standing on Shoulders</h2>

<p>Credit where it’s due: <a href="https://jammykam.wordpress.com/2016/01/06/specifying-query-and-parameters-for-sitecore-treelist-field-source/">Jammy Kam wrote a killer extension</a> back in 2016 that handled queryable TreeLists. I was using it on a multi-site Sitecore instance and kept hitting edge cases.</p>

<p>So I forked his approach and extended it. Here’s what I added and why.</p>

<p><br /></p>
<p><br /></p>

<h2 id="the-problem">The Problem</h2>

<p>Picture this: Multi-site Sitecore instance. Content authors need to pick related articles, but only from their current site. And only certain content types should even show up in the picker.</p>

<p>The vanilla TreeList? Nope. You’d have to either:</p>
<ul>
  <li>Hardcode every single path (maintenance nightmare)</li>
  <li>Create template variations for each site (also a nightmare)</li>
  <li>Write custom code every time (you know where this is going)</li>
</ul>

<p>And here’s the kicker—when content gets moved around (and it always does), authors would select items that were technically invalid. No warnings, no feedback. Just silent failures waiting to explode in production.</p>

<h3 id="what-was-breaking">What Was Breaking</h3>

<ol>
  <li><strong>Static only</strong> — Couldn’t use Sitecore queries to figure out the tree root dynamically</li>
  <li><strong>Template filtering was a mess</strong> — No way to say “show these types, but only let them select those types”</li>
  <li><strong>Zero validation</strong> — Authors had no clue when they picked something that was gonna cause problems later</li>
  <li><strong>GUID case sensitivity</strong> — Because nothing says “fun Friday afternoon” like debugging why <code class="language-plaintext highlighter-rouge">{abc-123}</code> doesn’t match <code class="language-plaintext highlighter-rouge">{ABC-123}</code></li>
</ol>

<p><br /></p>

<h2 id="the-fix">The Fix</h2>

<p>I extended the base TreeList control and added four things that made life way better:</p>

<h3 id="1-query-based-resolution">1. Query-Based Resolution</h3>

<p>Now you can use actual Sitecore queries to figure out where the tree root is:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c1">// Dynamic resolution based on current item</span>
<span class="n">DataSource</span><span class="p">=</span><span class="n">query</span><span class="p">:./</span><span class="n">ancestor</span><span class="p">-</span><span class="n">or</span><span class="p">-</span><span class="n">self</span><span class="p">::*[</span><span class="err">@</span><span class="n">@templateid</span><span class="p">=</span><span class="err">'</span><span class="p">{</span><span class="n">GUID</span><span class="p">}</span><span class="err">'</span><span class="p">]/*[</span><span class="err">@</span><span class="n">@name</span><span class="p">=</span><span class="err">'</span><span class="n">Content</span><span class="err">'</span><span class="p">]&amp;</span><span class="n">IncludeTemplatesForSelection</span><span class="p">={</span><span class="n">GUID</span><span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>No more hardcoded paths. The query runs in context of the current item, so it adapts automatically.</p>

<p><br /></p>

<h3 id="2-separate-display-vs-selection-filtering">2. Separate Display vs. Selection Filtering</h3>

<p>Sometimes you want authors to see certain items in the tree (for context), but not be able to actually select them.</p>

<ul>
  <li><strong>IncludeTemplatesForDisplay</strong> — What shows up in the tree</li>
  <li><strong>IncludeTemplatesForSelection</strong> — What they can actually pick</li>
</ul>

<p>Game changer for complex content hierarchies.</p>

<p><br /></p>

<h3 id="3-validation-warnings">3. Validation Warnings</h3>

<p>Here’s my favorite part: If an author has selected something that’s no longer valid (because content moved, or the query changed), they get a visual warning.</p>

<p>The field appends <code class="language-plaintext highlighter-rouge">[Not in selection list]</code> right there in the Content Editor. No more mystery bugs six months later.</p>

<p><br /></p>

<h3 id="4-template-id-normalization">4. Template ID Normalization</h3>

<p>That GUID case sensitivity bug? Fixed. All template IDs get normalized to uppercase before the query runs.</p>

<p>Turns out <code class="language-plaintext highlighter-rouge">{abc-123}</code> and <code class="language-plaintext highlighter-rouge">{ABC-123}</code> shouldn’t be different things. Who knew?</p>

<p><br /></p>
<p><br /></p>

<h2 id="how-it-actually-works">How It Actually Works</h2>

<p>Okay, let’s get into the weeds.</p>

<h3 id="the-three-overrides">The Three Overrides</h3>

<p>I’m hooking into three methods from the base <code class="language-plaintext highlighter-rouge">TreeList</code>:</p>

<ol>
  <li><strong>Source Property</strong> — Parses the custom parameters (display templates, selection templates, etc.)</li>
  <li><strong>DataSource Property</strong> — This is where the magic happens. Executes the query and figures out the tree root</li>
  <li><strong>GetHeaderValue</strong> — Injects the validation warnings when rendering selected items</li>
</ol>

<p>Nothing crazy, just strategic override points.</p>

<p><br /></p>

<h3 id="query-resolution-the-fun-part">Query Resolution (The Fun Part)</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">(</span><span class="n">rawSource</span><span class="p">.</span><span class="nf">StartsWith</span><span class="p">(</span><span class="s">"query:"</span><span class="p">,</span> <span class="n">StringComparison</span><span class="p">.</span><span class="n">OrdinalIgnoreCase</span><span class="p">))</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">current</span> <span class="p">=</span> <span class="n">Sitecore</span><span class="p">.</span><span class="n">Context</span><span class="p">.</span><span class="n">ContentDatabase</span><span class="p">.</span><span class="nf">GetItem</span><span class="p">(</span><span class="k">base</span><span class="p">.</span><span class="n">ItemID</span><span class="p">);</span>
    
    <span class="c1">// Normalize Template IDs to uppercase</span>
    <span class="n">queryPart</span> <span class="p">=</span> <span class="n">Regex</span><span class="p">.</span><span class="nf">Replace</span><span class="p">(</span>
        <span class="n">queryPart</span><span class="p">,</span>
        <span class="s">@"\{[0-9a-fA-F\-]{36}\}"</span><span class="p">,</span>
        <span class="n">m</span> <span class="p">=&gt;</span> <span class="n">m</span><span class="p">.</span><span class="n">Value</span><span class="p">.</span><span class="nf">ToUpperInvariant</span><span class="p">()</span>
    <span class="p">);</span>
    
    <span class="c1">// Execute query</span>
    <span class="n">obj</span> <span class="p">=</span> <span class="n">LookupSources</span><span class="p">.</span><span class="nf">GetItems</span><span class="p">(</span><span class="n">current</span><span class="p">,</span> <span class="n">queryPart</span><span class="p">).</span><span class="nf">FirstOrDefault</span><span class="p">();</span>
    
    <span class="k">return</span> <span class="n">obj</span><span class="p">?.</span><span class="n">Paths</span><span class="p">.</span><span class="n">FullPath</span> <span class="p">??</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<p><strong>Why this works:</strong></p>
<ul>
  <li>Query runs in the context of wherever the author is editing—so it’s always relative and dynamic</li>
  <li>The GUID normalization? That’s the regex on line 117. Saved me so many headaches</li>
  <li>If the query bombs or returns nothing, we just bail gracefully. Field still renders, no explosions</li>
</ul>

<p><br /></p>

<h3 id="validation-logic">Validation Logic</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">private</span> <span class="kt">bool</span> <span class="nf">IsItemInSelectionList</span><span class="p">(</span><span class="n">Item</span> <span class="n">item</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">dataSourcePath</span> <span class="p">=</span> <span class="k">this</span><span class="p">.</span><span class="n">DataSource</span><span class="p">;</span>
    <span class="kt">var</span> <span class="n">rootItem</span> <span class="p">=</span> <span class="n">Sitecore</span><span class="p">.</span><span class="n">Context</span><span class="p">.</span><span class="n">ContentDatabase</span><span class="p">?.</span><span class="nf">GetItem</span><span class="p">(</span><span class="n">dataSourcePath</span><span class="p">);</span>
    
    <span class="c1">// Item must be the root or a descendant</span>
    <span class="k">return</span> <span class="n">item</span><span class="p">.</span><span class="n">ID</span> <span class="p">==</span> <span class="n">rootItem</span><span class="p">.</span><span class="n">ID</span> <span class="p">||</span> 
           <span class="n">item</span><span class="p">.</span><span class="n">Paths</span><span class="p">.</span><span class="n">FullPath</span><span class="p">.</span><span class="nf">StartsWith</span><span class="p">(</span><span class="n">rootItem</span><span class="p">.</span><span class="n">Paths</span><span class="p">.</span><span class="n">FullPath</span><span class="p">,</span> 
               <span class="n">StringComparison</span><span class="p">.</span><span class="n">OrdinalIgnoreCase</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<p><strong>What this gives you:</strong></p>
<ul>
  <li>Authors see broken stuff immediately—no surprises during QA</li>
  <li>When content gets moved (and it will), you’ll know about it</li>
  <li>Zero silent failures. If something’s wrong, it screams at you in the UI</li>
</ul>

<p><br /></p>

<h3 id="error-handling-dont-skip-this">Error Handling (Don’t Skip This)</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">try</span>
<span class="p">{</span>
    <span class="n">obj</span> <span class="p">=</span> <span class="n">LookupSources</span><span class="p">.</span><span class="nf">GetItems</span><span class="p">(</span><span class="n">current</span><span class="p">,</span> <span class="n">queryPart</span><span class="p">).</span><span class="nf">FirstOrDefault</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">catch</span> <span class="p">(</span><span class="n">Exception</span> <span class="n">ex</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">logger</span><span class="p">?.</span><span class="nf">LogError</span><span class="p">(</span><span class="s">"QueryableTreeList field failed to execute query. Exception: {Exception}"</span><span class="p">,</span> <span class="n">ex</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Production reality:</strong></p>
<ul>
  <li>Malformed queries? Logged. You’ll see them in your logs, not in production at midnight</li>
  <li>Field still renders even if the query explodes. Authors can keep working</li>
  <li>No exceptions bubble up to users. It just… handles it</li>
</ul>

<p><br /></p>
<p><br /></p>

<h2 id="setting-it-up">Setting It Up</h2>

<h3 id="step-1-register-the-field-type">Step 1: Register the Field Type</h3>

<p>Head to <code class="language-plaintext highlighter-rouge">/sitecore/system/Field types/List Types/</code> and create a new field type.</p>

<p>Fill in:</p>
<ul>
  <li><strong>Name:</strong> QueryableTreeList</li>
  <li><strong>Assembly:</strong> YourNamespace.Foundation.SitecoreCMS</li>
  <li><strong>Class:</strong> YourNamespace.Foundation.SitecoreCMS.Custom.Controls.QueryableTreeList</li>
  <li><strong>Control:</strong> YourNamespace:QueryableTreeList</li>
</ul>

<p>Replace <code class="language-plaintext highlighter-rouge">YourNamespace</code> with your actual namespace (obviously).</p>

<p><br /></p>

<h3 id="step-2-use-it">Step 2: Use It</h3>

<p>In your template’s field source, throw in a query like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>DataSource=query:./ancestor-or-self::*[@@templateid='{SITE-TEMPLATE-ID}']/*[@@name='Articles']&amp;IncludeTemplatesForSelection={ARTICLE-TEMPLATE-ID}
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Swap in your actual template GUIDs. The query syntax is standard Sitecore query—if you’ve written them before, you’re good.</p>

<p><br /></p>

<h3 id="step-3-test-it">Step 3: Test It</h3>

<ol>
  <li>Create an item with your template</li>
  <li>Pop it open in Content Editor</li>
  <li>Check that the TreeList shows the right root (based on the query)</li>
  <li>Try selecting stuff. Move items around. Verify the warnings show up when they should</li>
</ol>

<p><br /></p>
<p><br /></p>

<h2 id="the-trade-offs-because-nothings-free">The Trade-offs (Because Nothing’s Free)</h2>

<h3 id="performance">Performance</h3>

<p>Real talk: This runs a query every time the field renders in Content Editor. If you’ve got deep tree structures or complex queries, it might get sluggish.</p>

<p><strong>What I did about it:</strong> Used it strategically. Not every TreeList needs this. Pick your battles. You could also cache query results if performance becomes an issue.</p>

<h3 id="learning-curve">Learning Curve</h3>

<p>Your content architects need to know Sitecore query syntax. Not everyone does.</p>

<p><strong>What helped:</strong> Documented the common patterns. Created a few field source templates they could copy-paste. After that, they were good.</p>

<h3 id="debugging">Debugging</h3>

<p>Malformed queries fail silently by design (see the error handling above). This is intentional—I’d rather have a field that doesn’t populate than one that crashes the Content Editor.</p>

<p><strong>But:</strong> You need good logging. When queries fail, you’ll want to know why. The logger calls saved me multiple times.</p>

<p><br /></p>
<p><br /></p>

<h2 id="what-happened-after-shipping-it">What Happened After Shipping It</h2>

<p>Deployed this to a big multi-site Sitecore instance. Here’s what I learned:</p>

<p><strong>Support tickets dropped 40%</strong> — Authors were catching their own mistakes before publishing. The validation warnings did exactly what they were supposed to do.</p>

<p><strong>Templates became reusable</strong> — No more creating variations for every site. One template, dynamic queries, done.</p>

<p><strong>The GUID normalization thing</strong> — Eliminated a whole category of “why is my list empty?” bugs. Those were a pain to debug before.</p>

<p><strong>Logging saved my ass</strong> — Multiple times. When someone wrote a bad query, I could see exactly what failed and why. Essential for multi-site setups.</p>

<p><br /></p>
<p><br /></p>

<h2 id="the-code">The Code</h2>

<p>Grab it from my <a href="https://github.com/marioarce/marioarce/blob/main/docs/code-samples/sitecore/QueryableTreeList.cs">GitHub repo</a>. It’s de-branded and ready to drop into your project.</p>

<p>What you get:</p>
<ul>
  <li>✅ Tested in production (the real test)</li>
  <li>✅ Error handling that doesn’t explode</li>
  <li>✅ Comments that actually explain why, not what</li>
  <li>✅ Works with Sitecore 10+ (maybe earlier, haven’t tested)</li>
</ul>

<p><br /></p>
<p><br /></p>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>Look, custom field types aren’t always the answer. But when the OOTB stuff doesn’t fit? Don’t hack around it. Extend it properly.</p>

<p>This QueryableTreeList solved real problems for real authors. The validation warnings alone paid for the development time within a month.</p>

<p><strong>The lesson:</strong> Invest in author experience. Happy authors = better content = fewer 3am production issues. Simple math.</p>

<p><br /></p>
<p><br /></p>

<h2 id="related-resources">Related Resources</h2>

<ul>
  <li><a href="https://jammykam.wordpress.com/2016/01/06/specifying-query-and-parameters-for-sitecore-treelist-field-source/">Original inspiration by Jammy Kam</a></li>
  <li><a href="https://doc.sitecore.com/xp/en/developers/latest/platform-administration-and-architecture/sitecore-query.html">Sitecore Query Syntax Documentation</a></li>
  <li><a href="https://doc.sitecore.com/xp/en/developers/latest/sitecore-experience-manager/custom-field-types.html">Custom Field Types in Sitecore</a></li>
</ul>]]></content><author><name>Mario Alberto Arce</name></author><category term="sitecore" /><category term="csharp" /><category term="custom-controls" /><category term="sitecore" /><category term="dotnet" /><category term="custom-field-types" /><category term="content-editor" /><summary type="html"><![CDATA[A production-ready extension of Sitecore's TreeList field that supports query-based data sources, template filtering, and validation warnings—solving real-world content authoring challenges.]]></summary></entry></feed>