Oqtane Event System — Hooking into the Framework

Oqtane Event System — Hooking into the Framework

OK, I’m still blocked from GitHub Copilot, so I still have more things to write about.
In this article, the topic that we’re going to see is the event system of Oqtane.For example, usually in most systems you want to hook up something when the application starts.
In XAF from Developer Express, which is my specialty (I mean, that’s the framework I really know well),
you have the DB Updater, which you can use to set up some initial data.
In Oqtane, you have the Module Manager, but there are also other types of events that you might need —
for example, when the user is created or when the user signs in for the first time.

So again, using the method that I explained in my previous article — the “OK, I have a doubt” method —
I basically let the guide of Copilot hike over my installation folder or even the Oqtane source code itself, and try to figure out how to do it.
That’s how I ended up using event subscribers.

In one of my prototypes, what I needed to do was detect when the user is created and then create some records in a different system
using that user’s information. So I’ll show an example of that type of subscriber, and I’ll actually share the
Oqtane Event Handling Guide here, which explains how you can hook up to system events.

I’m sure there are more events available, but this is what I’ve found so far and what I’ve tested.
I guess I’ll make a video about all these articles at some point, but right now, I’m kind of vibing with other systems.
Whenever I get blocked, I write something about my research with Oqtane.


Oqtane Event Handling Guide

Comprehensive guide to capturing and responding to system events in Oqtane

This guide explains how to handle events in Oqtane, particularly focusing on user authentication events (login, logout, creation)
and other system events. Learn to build modules that respond to framework events and create custom event-driven functionality.

Version: 1.0.0
Last Updated: October 3, 2025
Oqtane Version: 6.0+
Framework: .NET 9.0


1. Overview of Oqtane Event System

Oqtane uses a centralized event system based on the SyncManager that broadcasts events throughout the application when entities change.
This enables loose coupling between components and allows modules to respond to framework events without tight integration.

Key Components

  • SyncManager — Central event hub that broadcasts entity changes
  • SyncEvent — Event data containing entity information and action type
  • IEventSubscriber — Interface for objects that want to receive events
  • EventDistributorHostedService — Background service that distributes events to subscribers
Entity Changes → SyncManager → EventDistributorHostedService → IEventSubscriber Implementations
                     ↓
               SyncEvent Created → Distributed to All Event Subscribers
  

2. Event Types and Actions

SyncEvent Model


public class SyncEvent : EventArgs
{
    public int TenantId { get; set; }
    public int SiteId { get; set; }
    public string EntityName { get; set; }
    public int EntityId { get; set; }
    public string Action { get; set; }
    public DateTime ModifiedOn { get; set; }
}
  

Available Actions


public class SyncEventActions
{
    public const string Refresh = "Refresh";
    public const string Reload = "Reload";
    public const string Create = "Create";
    public const string Update = "Update";
    public const string Delete = "Delete";
}
  

Common Entity Names


public class EntityNames
{
    public const string User = "User";
    public const string Site = "Site";
    public const string Page = "Page";
    public const string Module = "Module";
    public const string File = "File";
    public const string Folder = "Folder";
    public const string Notification = "Notification";
}
  

3. Creating Event Subscribers

To handle events, implement IEventSubscriber and filter for the entities and actions you care about.
Subscribers are automatically discovered by Oqtane and injected with dependencies.


public class UserActivityEventSubscriber : IEventSubscriber
{
    private readonly ILogger<UserActivityEventSubscriber> _logger;

    public UserActivityEventSubscriber(ILogger<UserActivityEventSubscriber> logger)
    {
        _logger = logger;
    }

    public void EntityChanged(SyncEvent syncEvent)
    {
        if (syncEvent.EntityName != EntityNames.User)
            return;

        switch (syncEvent.Action)
        {
            case SyncEventActions.Create:
                _logger.LogInformation("User created: {UserId}", syncEvent.EntityId);
                break;
            case "Login":
                _logger.LogInformation("User logged in: {UserId}", syncEvent.EntityId);
                break;
        }
    }
}
  

4. User Authentication Events

Login, logout, and registration trigger SyncEvent notifications that you can capture to send notifications,
track user activity, or integrate with external systems.


public class LoginActivityTracker : IEventSubscriber
{
    private readonly ILogger<LoginActivityTracker> _logger;

    public LoginActivityTracker(ILogger<LoginActivityTracker> logger)
    {
        _logger = logger;
    }

    public void EntityChanged(SyncEvent syncEvent)
    {
        if (syncEvent.EntityName == EntityNames.User && syncEvent.Action == "Login")
        {
            _logger.LogInformation("User {UserId} logged in at {Time}", syncEvent.EntityId, syncEvent.ModifiedOn);
        }
    }
}
  

5. System Entity Events

Besides user events, you can track changes in entities like Pages, Files, and Modules.


public class PageAuditTracker : IEventSubscriber
{
    private readonly ILogger<PageAuditTracker> _logger;

    public PageAuditTracker(ILogger<PageAuditTracker> logger)
    {
        _logger = logger;
    }

    public void EntityChanged(SyncEvent syncEvent)
    {
        if (syncEvent.EntityName == EntityNames.Page && syncEvent.Action == SyncEventActions.Create)
        {
            _logger.LogInformation("Page created: {PageId}", syncEvent.EntityId);
        }
    }
}
  

6. Custom Module Events

You can create custom events in your own modules using ISyncManager.


public class BlogManager
{
    private readonly ISyncManager _syncManager;

    public BlogManager(ISyncManager syncManager)
    {
        _syncManager = syncManager;
    }

    public void PublishBlog(int blogId)
    {
        _syncManager.AddSyncEvent(
            new Alias { TenantId = 1, SiteId = 1 },
            "Blog",
            blogId,
            "Published"
        );
    }
}
  

7. Best Practices

  • Filter early — Always check the entity and action before processing.
  • Handle exceptions — Never throw unhandled exceptions inside EntityChanged.
  • Log properly — Use structured logging with context placeholders.
  • Keep it simple — Extract complex logic to testable services.

public void EntityChanged(SyncEvent syncEvent)
{
    try
    {
        if (syncEvent.EntityName == EntityNames.User && syncEvent.Action == "Login")
        {
            _logger.LogInformation("User {UserId} logged in", syncEvent.EntityId);
        }
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error processing event {Action}", syncEvent.Action);
    }
}
  

8. Summary

Oqtane’s event system provides a clean, decoupled way to respond to system changes.
It’s perfect for audit logs, notifications, custom workflows, and integrations.

  • Automatic discovery of subscribers
  • Centralized event distribution
  • Supports custom and system events
  • Integrates naturally with dependency injection
Understanding XtraReports: A Windows Forms Developer’s Guide

Understanding XtraReports: A Windows Forms Developer’s Guide

Introduction ?

If you’re familiar with Windows Forms development, transitioning to XtraReports will feel remarkably natural. This guide explores how XtraReports leverages familiar Windows Forms concepts while extending them for robust reporting capabilities.

? Quick Tip: Think of XtraReports as Windows Forms optimized for paper output instead of screen output!

A Personal Journey ✨

Microsoft released .NET Framework in late 2002. At the time, I was a VB6 developer, relying on Crystal Reports 7 for reporting. By 2003, my team was debating whether to transition to this new thing called .NET. We were concerned about VB6’s longevity—thinking it had just a couple more years left. How wrong we were! Even today, VB6 applications are still running in some places (it’s January 2, 2025, as I write this).

Back in the VB6 era, we used the Crystal Reports COM object to integrate reports. When we finally moved to .NET Framework, we performed some “black magic” to continue using our existing 700 reports across nine countries. The decision to fully embrace .NET was repeatedly delayed due to the sheer volume of reports we had to manage. Our ultimate goal was to unify our reporting and parameter forms within a single development environment.

This led us to explore other technologies. While considering Delphi, we discovered DevExpress. My boss procured our first DevExpress .NET license for Windows Forms, marking the start of my adventure with DevExpress and XtraReports. Initially, transitioning from the standalone Crystal Report Designer to the IDE-based XtraReports Designer was challenging. To better understand how XtraReports worked, I decided to write reports programmatically instead of using the visual designer.

Architectural Similarities ?️

XtraReports mirrors many fundamental Windows Forms concepts:

Source Destination
XtraReport Class Report Designer Surface
XtraReport Class Control Container
XtraReport Class Event System
XtraReport Class Properties Window
Control Container Labels & Text
Control Container Tables & Grids
Control Container Images & Charts
Report Designer Surface Control Toolbox
Report Designer Surface Design Surface
Report Designer Surface Preview Window

Like how Windows Forms applications start with a Form class, XtraReports begin with an XtraReport base class. Both serve as containers that can:

  • Host other controls
  • Manage layout
  • Handle events
  • Support data binding

Visual Designer Experience ?

The design experience remains consistent with Windows Forms:

Windows Forms XtraReports
Form Designer Report Designer
Toolbox Report Controls
Properties Window Properties Grid
Component Tray Component Tool

Control Ecosystem ?

XtraReports provides analogous controls to Windows Forms:


// Windows Forms
public partial class CustomerForm : Form
{
    private Label customerNameLabel;
    private DataGridView orderDetailsGrid;
}

// XtraReports
public partial class CustomerReport : XtraReport
{
    private XRLabel customerNameLabel;
    private XRTable orderDetailsTable;
}
    

Common control mappings:

  • Label ➡️ XRLabel
  • Panel ➡️ XRPanel
  • PictureBox ➡️ XRPictureBox
  • DataGridView ➡️ XRTable
  • GroupBox ➡️ Band
  • UserControl ➡️ Subreport

Data Binding Patterns ?

The data binding syntax maintains familiarity:


// Windows Forms data binding
customerNameLabel.DataBindings.Add("Text", customerDataSet, "Customers.Name");

// XtraReports data binding
customerNameLabel.ExpressionBindings.Add(
    new ExpressionBinding("Text", "[Name]"));
    

Code Architecture ?️

The code-behind model remains consistent:


public partial class CustomerReport : DevExpress.XtraReports.UI.XtraReport
{
    public CustomerReport()
    {
        InitializeComponent(); // Familiar Windows Forms pattern
    }

    private void CustomerReport_BeforePrint(object sender, PrintEventArgs e)
    {
        // Event handling similar to Windows Forms
        // Instead of Form_Load, we have Report_BeforePrint
    }
}
    

Key Differences ⚡

While similarities abound, important differences exist:

  1. Output Focus ?️
    • Windows Forms: Screen-based interaction
    • XtraReports: Print/export optimization
  2. Layout Model ?
    • Windows Forms: Flexible screen layouts
    • XtraReports: Page-based layouts with bands
  3. Control Behavior ?
    • Windows Forms: Interactive controls
    • XtraReports: Display-oriented controls
  4. Data Processing ?️
    • Windows Forms: Real-time data interaction
    • XtraReports: Batch data processing

Some Advices ?

  1. Design Philosophy
    
    // Think in terms of paper output
    public class InvoiceReport : XtraReport
    {
        protected override void OnBeforePrint(PrintEventArgs e)
        {
            // Calculate page breaks
            // Optimize for printing
        }
    }
                
  2. Layout Strategy
    • Use bands for logical grouping
    • Consider paper size constraints
    • Plan for different export formats
  3. Data Handling
    • Pre-process data when possible
    • Use calculated fields for complex logic
    • Consider subreports for complex layouts
Extending Interfaces in the Sync Framework: Best Practices and Trade-offs

Extending Interfaces in the Sync Framework: Best Practices and Trade-offs

In modern software development, extending the functionality of a framework while maintaining its integrity and usability can be a complex task. One common scenario involves extending interfaces to add new events or methods. In this post, we’ll explore the impact of extending interfaces within the Sync Framework, specifically looking at IDeltaStore and IDeltaProcessor interfaces to include SavingDelta and SavedDelta events, as well as ProcessingDelta and ProcessedDelta events. We’ll discuss the options available—extending existing interfaces versus adding new interfaces—and examine the side effects of each approach.

Background

The Sync Framework is designed to synchronize data across different data stores, ensuring consistency and integrity. The IDeltaStore interface typically handles delta storage operations, while the IDeltaProcessor interface manages delta (change) processing. To enhance the functionality, you might want to add events such as SavingDelta, SavedDelta, ProcessingDelta, and ProcessedDelta to these interfaces.

Extending Existing Interfaces

Extending existing interfaces involves directly adding new events or methods to the current interface definitions. Here’s an example:

public interface IDeltaStore {
    void SaveData(Data data);
    // New events
    event EventHandler<DeltaEventArgs> SavingDelta;
    event EventHandler<DeltaEventArgs> SavedDelta;
}

public interface IDeltaProcessor {
    void ProcessDelta(Delta delta);
    // New events
    event EventHandler<DeltaEventArgs> ProcessingDelta;
    event EventHandler<DeltaEventArgs> ProcessedDelta;
}

Pros of Extending Existing Interfaces

  • Simplicity: The existing implementations need to be updated to include the new functionality, making the overall design simpler.
  • Direct Integration: The new events are directly available in the existing interface, making them easy to use and understand within the current framework.

Cons of Extending Existing Interfaces

  • Breaks Existing Implementations: All existing classes implementing these interfaces must be updated to handle the new events. This can lead to significant refactoring, especially in large codebases.
  • Violates SOLID Principles: Adding new responsibilities to existing interfaces can violate the Single Responsibility Principle (SRP) and Interface Segregation Principle (ISP), leading to bloated interfaces.
  • Potential for Bugs: The necessity to modify all implementing classes increases the risk of introducing bugs and inconsistencies.

Adding New Interfaces

An alternative approach is to create new interfaces that extend the existing ones, encapsulating the new events. Here’s how you can do it:

public interface IDeltaStore {
    void SaveData(Data data);
}

public interface IDeltaStoreWithEvents : IDeltaStore {
    event EventHandler<DeltaEventArgs> SavingDelta;
    event EventHandler<DeltaEventArgs> SavedDelta;
}

public interface IDeltaProcessor {
    void ProcessDelta(Delta delta);
}

public interface IDeltaProcessorWithEvents : IDeltaProcessor {
    event EventHandler<DeltaEventArgs> ProcessingDelta;
    event EventHandler<DeltaEventArgs> ProcessedDelta;
}

Pros of Adding New Interfaces

  • Adheres to SOLID Principles: This approach keeps the existing interfaces clean and focused, adhering to the SRP and ISP.
  • Backward Compatibility: Existing implementations remain functional without modification, ensuring backward compatibility.
  • Flexibility: New functionality can be selectively adopted by implementing the new interfaces where needed.

Cons of Adding New Interfaces

  • Complexity: Introducing new interfaces can increase the complexity of the codebase, as developers need to understand and manage multiple interfaces.
  • Redundancy: There can be redundancy in code, where some classes might need to implement both the original and new interfaces.
  • Learning Curve: Developers need to be aware of and understand the new interfaces, which might require additional documentation and training.

Conclusion

Deciding between extending existing interfaces and adding new ones depends on your specific context and priorities. Extending interfaces can simplify the design but at the cost of violating SOLID principles and potentially breaking existing code. On the other hand, adding new interfaces preserves existing functionality and adheres to best practices but can introduce additional complexity.

In general, if maintaining backward compatibility and adhering to SOLID principles are high priorities, adding new interfaces is the preferred approach. However, if you are working within a controlled environment where updating existing implementations is manageable, extending the interfaces might be a viable option.

By carefully considering the trade-offs and understanding the implications of each approach, you can make an informed decision that best suits your project’s needs.