dotnet/wpf · error · InvalidOperationException

CannotChangeAfterSealed

CannotChangeAfterSealed

Error message

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

What it means

SeekStoryboard.Offset's setter throws InvalidOperationException once the SeekStoryboard object is sealed (read-only). ControllableStoryboardAction objects become sealed when they are in use inside a Storyboard/trigger tree, freezing their configuration.

Solutions

  1. Create a new SeekStoryboard instance for each seek and set Offset before use.
  2. Set Offset immediately after construction, before adding the action to any trigger or storyboard.
  3. If sharing is needed, keep a prototype and clone it per use.

Example fix

// before
sharedSeek.Offset = TimeSpan.FromSeconds(3); // throws after first use

// after
var seek = new SeekStoryboard { Storyboard = targetStoryboard, Offset = TimeSpan.FromSeconds(3), Origin = TimeSeekOrigin.BeginTime };
Defensive patterns

Strategy: type-guard

Validate before calling

if (seek.IsSealed) seek = new SeekStoryboard { Storyboard = seek.Storyboard, Offset = newOffset, Origin = seek.Origin };

Type guard

static SeekStoryboard EnsureMutable(SeekStoryboard s, TimeSpan newOffset) => s.IsSealed ? new SeekStoryboard { Storyboard = s.Storyboard, Offset = newOffset, Origin = s.Origin } : s;

Try / catch

try { seek.Offset = newOffset; } catch (InvalidOperationException) { seek = new SeekStoryboard { /* reconfigure */ }; }

Prevention

When it happens

Trigger: Setting the Offset property on a SeekStoryboard instance that has already been sealed — typically after it has been applied to a running storyboard or shared/attached in a template.

Common situations: Reusing a single SeekStoryboard instance across multiple seek operations from code; mutating a template-defined SeekStoryboard at runtime.

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

Appendix: source

Thrown at src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Media/Animation/SeekStoryboard.cs:37

    /// </summary>
    public sealed class SeekStoryboard : ControllableStoryboardAction
{
    /// <summary>
    ///     A time offset to use for this action.  If it is never explicitly
    /// specified, it will be zero.
    /// </summary>
    // [DefaultValue(TimeSpan.Zero)] - not usable because TimeSpan.Zero is not a constant expression.
    public TimeSpan Offset
    {
        get
        {
            return _offset;
        }
        set
        {
            if (IsSealed)
            {
                throw new InvalidOperationException(SR.Format(SR.CannotChangeAfterSealed, "SeekStoryboard"));
            }
            // TimeSpan is a struct and can't be null - hence no ArgumentNullException check.
            _offset = value;
        }
    }

    /// <summary>
    /// This method is used by TypeDescriptor to determine if this property should
    /// be serialized.
    /// </summary>
    // Because we can't use [DefaultValue(TimeSpan.Zero)] - TimeSpan.Zero is not a constant expression.
    [EditorBrowsable(EditorBrowsableState.Never)]
    public bool ShouldSerializeOffset()
    {
        return !(TimeSpan.Zero.Equals(_offset));
    }
    

View on GitHub (pinned to 81131a70a4)