dotnet/wpf · error · InvalidOperationException

SR.TriggerActionMustBelongToASingleTrigger

Error message

SR.TriggerActionMustBelongToASingleTrigger

What it means

TriggerAction.Seal(TriggerBase) enforces that a TriggerAction instance belongs to exactly one trigger. If the action is already sealed and a different containingTrigger tries to seal it, InvalidOperationException with SR.TriggerActionMustBelongToASingleTrigger is thrown, because sharing a sealed action across triggers would corrupt its owning-trigger state.

Solutions

  1. Create a separate TriggerAction instance for each trigger that needs one.
  2. If sharing is intended, create a factory method returning a new action per trigger.
  3. Check action.IsSealed before adding; if sealed and already owned by another trigger, construct a copy.

Example fix

// before
var action = new BeginStoryboard(storyboard);
triggerA.Actions.Add(action);
triggerB.Actions.Add(action); // throws on seal of triggerB
// after
triggerA.Actions.Add(new BeginStoryboard(storyboard));
triggerB.Actions.Add(new BeginStoryboard(storyboard));
Defensive patterns

Strategy: validation

Validate before calling

if (action.IsSealed) throw new InvalidOperationException("Create a new TriggerAction instance for each trigger");

Type guard

static bool CanAttach(TriggerAction a, TriggerBase t) => a != null && (!a.IsSealed || true /* cannot inspect owner; assume not shared */);

Try / catch

try { trigger.Actions.Add(action); }
catch (InvalidOperationException) { trigger.Actions.Add(CloneAction(action)); }

Prevention

When it happens

Trigger: Reusing one BeginStoryboard/TriggerAction instance in two different EventTriggers (or a trigger and another trigger) — the second Seal attempt with a different containing trigger throws.

Common situations: Caching TriggerAction objects (e.g. one BeginStoryboard) and adding the same instance to multiple EventTriggers across styles or controls.

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/af269da2024bb837. Report an issue: GitHub.

Appendix: source

Thrown at src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/TriggerAction.cs:87

            get
            {
                return _containingTrigger;
            }
        }

        /// <summary>
        ///     Seal this TriggerAction to prevent further updates
        /// </summary>
        /// <remarks>
        ///     TriggerActionCollection will call this method to seal individual
        /// TriggerAction objects.  We do some check here then call the
        /// parameter-less Seal() so subclasses can also do what they need to do.
        /// </remarks>
        internal void Seal( TriggerBase containingTrigger )
        {
            if( IsSealed && containingTrigger != _containingTrigger )
            {
                throw new InvalidOperationException(SR.TriggerActionMustBelongToASingleTrigger);
            }
            _containingTrigger = containingTrigger;
            Seal();
        }

        /// <summary>
        ///     A derived class overrideing Seal() should set object state such
        /// that further changes are not allowed.  This is also a time to make
        /// validation checks to see if all parameters make sense.
        /// </summary>
        internal override void Seal()
        {
            if( IsSealed )
            {
                throw new InvalidOperationException(SR.TriggerActionAlreadySealed);
            }
            base.Seal();
        }

View on GitHub (pinned to 81131a70a4)