dotnet/wpf · error · InvalidOperationException
SR.Format(SR.StyleBasedOnHasLoop)
Error message
SR.Format(SR.StyleBasedOnHasLoop)
What it means
CheckForCircularBasedOnReferences walks the BasedOn chain during Seal and detects a style that appears twice in the hierarchy, i.e. a cycle (A BasedOn B BasedOn ... BasedOn A). Cycles would cause infinite recursion during style resolution, so Seal throws InvalidOperationException (SR.StyleBasedOnHasLoop).
Solutions
- Break the cycle: make one style's BasedOn null or point it at a genuinely separate base style.
- Construct style hierarchies bottom-up (base styles first) and never re-parent a style that is already an ancestor.
- Add an assertion/loop check in code that assigns BasedOn dynamically.
Example fix
// before styleA.BasedOn = styleB; styleB.BasedOn = styleA; // cycle // after styleA.BasedOn = null; styleB.BasedOn = styleA;
Defensive patterns
Strategy: validation
Validate before calling
var seen = new HashSet<Style>();
for (var s = style.BasedOn; s != null; s = s.BasedOn)
if (!seen.Add(s)) throw new InvalidOperationException("BasedOn cycle detected before Seal."); Type guard
static bool HasBasedOnLoop(Style s) { var seen = new HashSet<Style>(); for (var b = s.BasedOn; b != null; b = b.BasedOn) if (!seen.Add(b)) return true; return false; } Try / catch
try { style.Seal(); }
catch (InvalidOperationException ex) when (ex.Message.Contains("loop")) { log.Error("Circular BasedOn", ex); } Prevention
- Build style hierarchies bottom-up and never re-parent ancestors.
- Track assignments of BasedOn in code-generated style graphs.
- Walk the BasedOn chain before sealing to detect cycles early.
When it happens
Trigger: Setting styleA.BasedOn = styleB and later styleB.BasedOn = styleA (directly or through a longer chain) before either is sealed; programmatically rewiring BasedOn in a loop over styles.
Common situations: Code-generated style graphs where parents are assigned in dependency order incorrectly; late mutation of BasedOn on cached styles creating a back-reference; merging dictionaries where two styles reference each other.
Related errors
- SR.Format(SR.MustBaseOnStyleOfABaseType, _targetType.Name)
- SR.StyleCannotBeBasedOnSelf
- SR.CircularOwnerChild
- SR.Format(SR.CannotChangeAfterSealed, "EventTrigger")
- SR.Format(SR.CannotChangeAfterSealed, "SetterBase")
AI-assisted analysis of dotnet/wpf@81131a70a4 (2026-09-14).
Data as JSON: /api/errors/4c28ebec711894f2.
Report an issue: GitHub.
Appendix: source
Thrown at src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Style.cs:577
/// <remarks>
/// Classic "when did we enter the cycle" problem where we don't know
/// what to start remembering and what to check against. Brute-
/// force approach here is to remember everything with a stack
/// and do a linear comparison through everything. Since the Style
/// BasedOn hierarchy is not expected to be large, this should be OK.
/// </remarks>
private void CheckForCircularBasedOnReferences()
{
Stack basedOnHierarchy = new Stack(10); // 10 because that's the default value (see MSDN) and the perf team wants us to specify something.
Style latestBasedOn = this;
while( latestBasedOn != null )
{
if( basedOnHierarchy.Contains( latestBasedOn ) )
{
// Uh-oh. We've seen this Style before. This means
// the BasedOn hierarchy contains a loop.
throw new InvalidOperationException(SR.Format(
SR.StyleBasedOnHasLoop));
// Debugging note: If we stop here, the basedOnHierarchy
// object is still alive and we can browse through it to
// see what we've explored. (This does not apply if
// somebody catches this exception and re-throws.)
}
// Haven't seen it, push on stack and go to next level.
basedOnHierarchy.Push( latestBasedOn );
latestBasedOn = latestBasedOn.BasedOn;
}
return;
}
// Iterates through the setters collection and adds the EventSetter information into
// an EventHandlersStore for easy and fast retrieval during event routing. Also addsView on GitHub (pinned to 81131a70a4)