📋 Content Models
Event Model
📖 Docs

Event Model - Optimizely CMS 12

Exam Area: Content Area 5 – Content Models (25%)
Reference: https://docs.developers.optimizely.com/content-management-system/docs/event-management


1. Content Event Lifecycle

Create/Edit Content
    │
    ├── SavingContent (before)
    ├── SavedContent (after)
    │
    ├── PublishingContent (before)
    ├── PublishedContent (after)
    │
    ├── DeletingContent (before)  
    └── DeletedContent (after)

2. All Events in IContentEvents

EventTrigger
LoadingContentBefore loading content
LoadedContentAfter loading content
LoadingChildrenBefore loading children
LoadedChildrenAfter loading children
SavingContentBefore saving
SavedContentAfter saving
PublishingContentBefore publishing
PublishedContentAfter publishing
DeletingContentBefore deleting
DeletedContentAfter deleting
MovingContentBefore moving
MovedContentAfter moving
RequestingApprovalRequesting approval
RejectingContentRejecting content
CheckingInContentChecking in content
CheckedInContentAfter check in

3. SaveAction Enum

private void OnSaving(object sender, ContentEventArgs e)
{
    switch (e.Action)
    {
        case SaveAction.Publish:
            // Publishing
            break;
        case SaveAction.Save:
            // Draft save
            break;
        case SaveAction.CheckOut:
            // Checkout for editing
            break;
        case SaveAction.CheckIn:
            // Check in
            break;
        case SaveAction.RequestApproval:
            // Send request for approval
            break;
        case SaveAction.Reject:
            // Reject
            break;
        case SaveAction.SchedulePublish:
            // Scheduled publish
            break;
        case SaveAction.ForceCurrentVersion:
            // Force version
            break;
    }
}

4. Practical Event Use Cases

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class ContentEventHandlers : IInitializableModule
{
    private IContentEvents _events;

    public void Initialize(InitializationEngine context)
    {
        _events = context.Locate.Advanced.GetInstance<IContentEvents>();
        
        _events.PublishedContent += OnPublished;
        _events.SavingContent += OnSaving;
        _events.MovedContent += OnMoved;
        _events.DeletedContent += OnDeleted;
    }

    public void Uninitialize(InitializationEngine context)
    {
        _events.PublishedContent -= OnPublished;
        _events.SavingContent -= OnSaving;
        _events.MovedContent -= OnMoved;
        _events.DeletedContent -= OnDeleted;
    }

    // Use case 1: Sync with external system on publish
    private void OnPublished(object sender, ContentEventArgs e)
    {
        if (e.Content is ProductPage product)
        {
            // Push to product catalog API
            ExternalCatalogService.SyncProduct(product);
        }
    }

    // Use case 2: Validate before saving
    private void OnSaving(object sender, ContentEventArgs e)
    {
        if (e.Content is ArticlePage article)
        {
            if (e.Action == SaveAction.Publish && string.IsNullOrWhiteSpace(article.MetaDescription))
            {
                e.CancelAction = true;
                e.CancelReason = "Meta description is required before publishing";
            }
        }
    }

    // Use case 3: Update search index
    private void OnPublished_SearchIndex(object sender, ContentEventArgs e)
    {
        if (e.Content is ISearchable searchable)
        {
            SearchIndexService.UpdateIndex(e.ContentLink);
        }
    }

    // Use case 4: Audit log
    private void OnDeleted(object sender, ContentEventArgs e)
    {
        AuditLogger.Log(`Content ${e.ContentLink} deleted by ${HttpContext.Current?.User?.Identity?.Name}`);
    }
    
    // Use case 5: Clear cache on move
    private void OnMoved(object sender, ContentEventArgs e)
    {
        CacheManager.RemoveByContentLink(e.ContentLink);
    }
}

5. CreatedContent Event (IContentEvents extension)

// Distinguish create vs save:
private void OnSaving(object sender, ContentEventArgs e)
{
    var isNew = e.ContentLink == ContentReference.EmptyReference;
    
    if (isNew)
    {
        // New content - set default values
        if (e.Content is ArticlePage article)
        {
            article.Author = GetCurrentUser();
        }
    }
}

6. ContentEvents vs IContentEvents

// ContentEvents is the concrete class (not used directly)
// IContentEvents is the interface to use

public class MyHandler
{
    private readonly IContentEvents _contentEvents;

    public MyHandler(IContentEvents contentEvents)  // Inject interface
    {
        _contentEvents = contentEvents;
    }
}

7. Remote Events (Cluster)

// IRemoteEventService for cross-server events
public class MyRemoteEventHandler
{
    private readonly IRemoteEventService _remoteEventService;

    public MyRemoteEventHandler(IRemoteEventService remoteEventService)
    {
        _remoteEventService = remoteEventService;
    }
    
    public void BroadcastCacheInvalidation(ContentReference contentLink)
    {
        // Send event to all server instances
        _remoteEventService.RaiseEvent(
            "CacheInvalidation",
            new CacheEventData { ContentLink = contentLink });
    }
}

Review Questions

  1. PublishingContent vs PublishedContent - when to use each? (Publishing: before = can cancel; Published: after = cannot cancel)
  2. How do you cancel a publish? (e.CancelAction = true; e.CancelReason = "...")
  3. Why check e.Action in SavingContent? (To distinguish Save draft vs Publish vs CheckIn)
  4. Where are event handlers registered? (In IInitializableModule.Initialize())
  5. Why MUST you unsubscribe in Uninitialize()? (Module may restart; avoids duplicate handlers)