dotnet/wpf · error · InvalidOperationException

SR.Format(SR.CannotChangeAfterSealed, "TriggerCollection")

Error message

SR.Format(SR.CannotChangeAfterSealed, "TriggerCollection")

What it means

A TriggerCollection becomes sealed once the owning FrameworkElement/FrameworkContentElement (via its Style or template) has been initialized and the collection is no longer mutable. Any attempt to Clear, Insert, Remove, or Set items in a sealed collection throws this InvalidOperationException from CheckSealed. This protects trigger state that the framework depends on after styling has been applied.

Solutions

  1. Modify triggers before the element is styled/loaded, e.g. in XAML or before adding the element to the visual tree
  2. Create a new collection/element if post-load trigger changes are required
  3. Trigger style re-evaluation by rebuilding the Style and reassigning it rather than mutating a sealed collection
  4. Check collection.IsSealed before mutating and branch to a rebuild path

Example fix

// before
protected override void OnLoaded(EventArgs e) {
    Triggers.Clear(); // sealed -> throws
}
// after
protected override void OnInitialized(EventArgs e) {
    Triggers.Add(new Trigger { Property = IsMouseOverProperty, Value = true });
}
Defensive patterns

Strategy: validation

Validate before calling

if (!triggers.IsSealed) { triggers.Add(newTrigger); } else { /* rebuild element or style */ }

Type guard

bool CanMutate(TriggerCollection c) => !c.IsSealed;

Try / catch

try { triggers.Add(t); } catch (InvalidOperationException ex) when (ex.Message.Contains("TriggerCollection")) { /* rebuild collection instead */ }

Prevention

When it happens

Trigger: Calling triggers.Clear(), triggers.Add(...), triggers.Remove(...), or triggers[i] = ... on a TriggerCollection after it has been sealed, typically because the element's style/template context has been established and the collection instance is shared with the style.

Common situations: Mutating element.Triggers at runtime after the element is loaded/styled; sharing one TriggerCollection instance between multiple elements; modifying triggers inside a Loaded handler or after ApplyTemplate.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of dotnet/wpf@81131a70a4 (2026-09-14). Data as JSON: /api/errors/9d90b7b10dfcb386. Report an issue: GitHub.

Appendix: source

Thrown at src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/TriggerCollection.cs:131

        }
        
        // This may be null (i.e. when used in a style or template)
        internal FrameworkElement Owner
        {
            get { return _owner; }
        }


        
        #endregion InternalMethods

        #region PrivateMethods

        private void CheckSealed()
        {
            if (_sealed)
            {
                throw new InvalidOperationException(SR.Format(SR.CannotChangeAfterSealed, "TriggerCollection"));
            }
        }

        private void TriggerBaseValidation(TriggerBase triggerBase)
        {
            ArgumentNullException.ThrowIfNull(triggerBase);
        }

        // Called by GenericCollection.tb when a trigger is added to the collection.
        // We use this opportunity to hook it into the tree.
        private void OnAdd( TriggerBase triggerBase )
        {
            // If we don't have an Owner (the Style/Template case), or the
            // element isn't initialized yet, we don't need to do anything
            if (Owner != null && Owner.IsInitialized)
            {
                EventTrigger.ProcessOneTrigger(Owner, triggerBase);
            }

View on GitHub (pinned to 81131a70a4)