Event System
Updated 2 days agoEvent System
Learn how to use ZUI's event system to respond to UI changes and updates.
📋 Table of Contents
- Overview
- OnButtonsChanged Event
- Event Usage Patterns
- Best Practices
- Common Use Cases
- Troubleshooting
Overview
ZUI provides an event system that allows mods to respond to UI changes. Currently, the primary event is OnButtonsChanged, which fires when UI buttons are updated.
Available Events
| Event | Description | When It Fires |
|---|---|---|
OnButtonsChanged |
Triggered when UI buttons are added or updated | After any plugin registers/updates buttons |
Why Use Events?
Events are useful for:
- 🔄 Refreshing UI state
- 📊 Updating dynamic content
- 🔔 Responding to UI changes from other mods
- 🎯 Synchronizing multiple UI elements
- 📈 Tracking UI modifications
OnButtonsChanged Event
The OnButtonsChanged event fires whenever buttons are registered or modified in the ZUI system.
Event Signature
public static event Action OnButtonsChanged;
Subscribing to the Event
Hard Dependency Approach
If using ZUI as a hard dependency:
using ZUI.API;
public class Plugin : BasePlugin
{
public override void Load()
{
// Subscribe to the event
ZUI.OnButtonsChanged += OnUIUpdated;
Log.LogInfo("Subscribed to ZUI events");
}
private void OnUIUpdated()
{
Log.LogInfo("UI buttons were updated!");
// Your response logic here
}
}
Soft Dependency Approach (Recommended)
If using ZUI as a soft dependency with reflection:
private static Type _zuiType;
private static EventInfo _onButtonsChangedEvent;
private bool InitZUI()
{
if (!IL2CPPChainloader.Instance.Plugins.ContainsKey("Zanakinz.ZUI"))
return false;
var assembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "ZUI");
if (assembly == null) return false;
_zuiType = assembly.GetType("ZUI.API.ZUI");
if (_zuiType == null) return false;
// Get the event
_onButtonsChangedEvent = _zuiType.GetEvent("OnButtonsChanged",
BindingFlags.Public | BindingFlags.Static);
return _onButtonsChangedEvent != null;
}
private void SubscribeToEvents()
{
if (_onButtonsChangedEvent == null) return;
try
{
// Create delegate for the event
var handler = Delegate.CreateDelegate(
_onButtonsChangedEvent.EventHandlerType,
this,
nameof(OnUIUpdated)
);
// Subscribe to event
_onButtonsChangedEvent.AddEventHandler(null, handler);
Log.LogInfo("Subscribed to OnButtonsChanged event");
}
catch (Exception ex)
{
Log.LogError($"Failed to subscribe to events: {ex.Message}");
}
}
private void OnUIUpdated()
{
Log.LogInfo("UI buttons were updated!");
// Your response logic here
}
public override void Load()
{
if (InitZUI())
{
RegisterUI();
SubscribeToEvents();
}
}
Unsubscribing from Events
Good practice to unsubscribe when your plugin unloads:
Hard Dependency
public override bool Unload()
{
ZUI.OnButtonsChanged -= OnUIUpdated;
Log.LogInfo("Unsubscribed from ZUI events");
return true;
}
Soft Dependency
private Delegate _eventHandler;
private void SubscribeToEvents()
{
if (_onButtonsChangedEvent == null) return;
try
{
_eventHandler = Delegate.CreateDelegate(
_onButtonsChangedEvent.EventHandlerType,
this,
nameof(OnUIUpdated)
);
_onButtonsChangedEvent.AddEventHandler(null, _eventHandler);
Log.LogInfo("Subscribed to OnButtonsChanged event");
}
catch (Exception ex)
{
Log.LogError($"Failed to subscribe to events: {ex.Message}");
}
}
public override bool Unload()
{
if (_onButtonsChangedEvent != null && _eventHandler != null)
{
try
{
_onButtonsChangedEvent.RemoveEventHandler(null, _eventHandler);
Log.LogInfo("Unsubscribed from ZUI events");
}
catch (Exception ex)
{
Log.LogError($"Failed to unsubscribe: {ex.Message}");
}
}
return true;
}
Event Usage Patterns
Pattern 1: Update UI on Changes
Refresh your mod's UI when other mods make changes:
private void OnUIUpdated()
{
Log.LogInfo("UI updated, refreshing my mod's display");
UpdateMyModUI();
}
private void UpdateMyModUI()
{
// Re-register or update your UI elements
Call("SetPlugin", "MyMod");
Call("SetTargetWindow", "MyPanel");
// Update dynamic content
var playerCount = GetPlayerCount();
Call("AddText", $"Players Online: {playerCount}", 20f, 50f);
}
Pattern 2: Track UI State
Keep track of how many times the UI has been modified:
private int _uiUpdateCount = 0;
private void OnUIUpdated()
{
_uiUpdateCount++;
Log.LogInfo($"UI has been updated {_uiUpdateCount} times");
// Perform action every N updates
if (_uiUpdateCount % 10 == 0)
{
Log.LogInfo("UI updated 10 times, performing maintenance...");
PerformUIMaintenanceTask();
}
}
Pattern 3: Synchronize Multiple Windows
Keep multiple custom windows in sync:
private void OnUIUpdated()
{
Log.LogInfo("Synchronizing all windows");
// Update main panel
UpdateMainPanel();
// Update status panel
UpdateStatusPanel();
// Update settings panel
UpdateSettingsPanel();
}
private void UpdateMainPanel()
{
Call("SetTargetWindow", "MainPanel");
// Update content...
}
private void UpdateStatusPanel()
{
Call("SetTargetWindow", "StatusPanel");
// Update content...
}
Pattern 4: Conditional Updates
Only respond to events under certain conditions:
private bool _isEnabled = true;
private DateTime _lastUpdate = DateTime.MinValue;
private void OnUIUpdated()
{
// Ignore if disabled
if (!_isEnabled)
{
Log.LogDebug("UI update ignored (disabled)");
return;
}
// Rate limiting - only update once per second
if ((DateTime.Now - _lastUpdate).TotalSeconds < 1.0)
{
Log.LogDebug("UI update ignored (rate limited)");
return;
}
_lastUpdate = DateTime.Now;
Log.LogInfo("Processing UI update");
UpdateMyModUI();
}
Best Practices
✅ DO:
1. Keep event handlers lightweight
private void OnUIUpdated()
{
// Quick, simple operations
Log.LogInfo("UI updated");
_needsRefresh = true; // Set flag for later processing
}
2. Use error handling
private void OnUIUpdated()
{
try
{
UpdateMyModUI();
}
catch (Exception ex)
{
Log.LogError($"Error in OnUIUpdated: {ex.Message}");
}
}
3. Unsubscribe when done
public override bool Unload()
{
ZUI.OnButtonsChanged -= OnUIUpdated;
return true;
}
4. Use rate limiting if needed
private DateTime _lastEventTime = DateTime.MinValue;
private void OnUIUpdated()
{
if ((DateTime.Now - _lastEventTime).TotalSeconds < 0.5)
return; // Ignore rapid-fire events
_lastEventTime = DateTime.Now;
// Process update...
}
❌ DON'T:
1. Don't do heavy processing in event handlers
// ❌ Bad
private void OnUIUpdated()
{
// Expensive operation directly in event
for (int i = 0; i < 10000; i++)
{
PerformExpensiveCalculation();
}
}
// ✅ Good
private bool _needsUpdate = false;
private void OnUIUpdated()
{
_needsUpdate = true; // Set flag
}
public override void Update()
{
if (_needsUpdate)
{
PerformExpensiveCalculation();
_needsUpdate = false;
}
}
2. Don't cause infinite loops
// ❌ Bad - This will cause infinite event loop!
private void OnUIUpdated()
{
Call("AddButton", "Test", ".test"); // Triggers OnButtonsChanged again!
}
// ✅ Good - Use flag to prevent recursion
private bool _isUpdating = false;
private void OnUIUpdated()
{
if (_isUpdating) return;
_isUpdating = true;
try
{
Call("AddButton", "Test", ".test");
}
finally
{
_isUpdating = false;
}
}
3. Don't forget to unsubscribe
// ❌ Bad - Memory leak potential
public override bool Unload()
{
return true; // Forgot to unsubscribe!
}
// ✅ Good
public override bool Unload()
{
ZUI.OnButtonsChanged -= OnUIUpdated;
return true;
}
Common Use Cases
Use Case 1: Dynamic Player Count Display
Update UI to show current player count:
private void OnUIUpdated()
{
RefreshPlayerCount();
}
private void RefreshPlayerCount()
{
Call("SetPlugin", "PlayerTracker");
Call("SetTargetWindow", "StatusPanel");
Call("SetUI", 300, 150);
int playerCount = GetCurrentPlayerCount();
Call("AddText", $"Players: {playerCount}/50", 20f, 50f);
}
private int GetCurrentPlayerCount()
{
// Your logic to get player count
return 24;
}
Use Case 2: Mod Compatibility Check
Detect when other mods register UI:
private HashSet<string> _detectedMods = new HashSet<string>();
private void OnUIUpdated()
{
CheckForNewMods();
}
private void CheckForNewMods()
{
// Logic to detect new mod registrations
var newMod = DetectNewModRegistration();
if (newMod != null && _detectedMods.Add(newMod))
{
Log.LogInfo($"Detected new mod with UI: {newMod}");
UpdateCompatibilityList();
}
}
Use Case 3: UI State Persistence
Save UI state when changes occur:
private void OnUIUpdated()
{
SaveUIState();
}
private void SaveUIState()
{
var state = new UIState
{
LastUpdate = DateTime.Now,
ButtonCount = GetButtonCount(),
WindowPositions = GetWindowPositions()
};
// Save to file or config
SaveToConfig(state);
Log.LogInfo("UI state saved");
}
Use Case 4: Debug Logging
Track all UI modifications for debugging:
private int _eventCount = 0;
private void OnUIUpdated()
{
_eventCount++;
Log.LogDebug($"[{_eventCount}] OnButtonsChanged fired");
Log.LogDebug($" Time: {DateTime.Now:HH:mm:ss.fff}");
Log.LogDebug($" Stack trace: {Environment.StackTrace}");
// Additional debug info
LogCurrentUIState();
}
private void LogCurrentUIState()
{
// Log current UI elements, window states, etc.
Log.LogDebug($" Current windows: {GetWindowCount()}");
Log.LogDebug($" Current buttons: {GetButtonCount()}");
}
Troubleshooting
Event doesn't fire
Possible causes:
- Event subscription failed
- ZUI not initialized
- Wrong event name
Solutions:
✅ Add subscription logging:
private void SubscribeToEvents()
{
try
{
ZUI.OnButtonsChanged += OnUIUpdated;
Log.LogInfo("✓ Successfully subscribed to OnButtonsChanged");
}
catch (Exception ex)
{
Log.LogError($"✗ Failed to subscribe: {ex.Message}");
}
}
✅ Test event manually:
// Trigger event by registering something
Call("AddButton", "Test", ".test");
// Event should fire now
Event fires too many times
Cause: Multiple mods registering UI elements triggers the event multiple times.
Solution: Use rate limiting or debouncing:
private DateTime _lastProcessed = DateTime.MinValue;
private const double MIN_INTERVAL = 0.5; // 500ms minimum between processing
private void OnUIUpdated()
{
var elapsed = (DateTime.Now - _lastProcessed).TotalSeconds;
if (elapsed < MIN_INTERVAL)
{
Log.LogDebug($"Event ignored (debounced, {elapsed:F2}s since last)");
return;
}
_lastProcessed = DateTime.Now;
ProcessUIUpdate();
}
Memory leak / Performance degradation
Cause: Not unsubscribing from events when mod unloads.
Solution: Always unsubscribe:
public override bool Unload()
{
if (_eventHandler != null && _onButtonsChangedEvent != null)
{
_onButtonsChangedEvent.RemoveEventHandler(null, _eventHandler);
_eventHandler = null;
Log.LogInfo("Event handlers cleaned up");
}
return true;
}
Exception in event handler
Cause: Unhandled exception in your event handler code.
Solution: Wrap in try-catch:
private void OnUIUpdated()
{
try
{
UpdateMyModUI();
}
catch (Exception ex)
{
Log.LogError($"Error in OnUIUpdated: {ex.Message}");
Log.LogError($"Stack trace: {ex.StackTrace}");
}
}
Complete Example
Here's a complete example using events with soft dependency:
using BepInEx;
using BepInEx.Logging;
using BepInEx.Unity.IL2CPP;
using System;
using System.Linq;
using System.Reflection;
namespace MyMod
{
[BepInPlugin("com.example.mymod", "My Mod", "1.0.0")]
[BepInDependency("Zanakinz.ZUI", BepInDependency.DependencyFlags.SoftDependency)]
public class Plugin : BasePlugin
{
private static Type _zuiType;
private static EventInfo _onButtonsChangedEvent;
private static Delegate _eventHandler;
private int _updateCount = 0;
public override void Load()
{
if (InitZUI())
{
RegisterUI();
SubscribeToEvents();
}
}
private bool InitZUI()
{
if (!IL2CPPChainloader.Instance.Plugins.ContainsKey("Zanakinz.ZUI"))
return false;
var assembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "ZUI");
if (assembly == null) return false;
_zuiType = assembly.GetType("ZUI.API.ZUI");
if (_zuiType == null) return false;
_onButtonsChangedEvent = _zuiType.GetEvent("OnButtonsChanged",
BindingFlags.Public | BindingFlags.Static);
return _onButtonsChangedEvent != null;
}
private void SubscribeToEvents()
{
if (_onButtonsChangedEvent == null) return;
try
{
_eventHandler = Delegate.CreateDelegate(
_onButtonsChangedEvent.EventHandlerType,
this,
nameof(OnUIUpdated)
);
_onButtonsChangedEvent.AddEventHandler(null, _eventHandler);
Log.LogInfo("Subscribed to OnButtonsChanged");
}
catch (Exception ex)
{
Log.LogError($"Event subscription failed: {ex.Message}");
}
}
private void OnUIUpdated()
{
try
{
_updateCount++;
Log.LogInfo($"UI updated (event #{_updateCount})");
// Your response logic here
RefreshUIState();
}
catch (Exception ex)
{
Log.LogError($"Error in OnUIUpdated: {ex.Message}");
}
}
private void RefreshUIState()
{
// Update your UI elements
Log.LogInfo("Refreshing UI state...");
}
private void RegisterUI()
{
Call("SetPlugin", "My Mod");
Call("SetTargetWindow", "Main");
Call("AddCategory", "Features");
Call("AddButton", "Test", ".test");
}
private void Call(string methodName, params object[] args)
{
if (_zuiType == null) return;
var method = _zuiType.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(m => m.Name == methodName &&
m.GetParameters().Length == args.Length);
method?.Invoke(null, args);
}
public override bool Unload()
{
if (_onButtonsChangedEvent != null && _eventHandler != null)
{
_onButtonsChangedEvent.RemoveEventHandler(null, _eventHandler);
Log.LogInfo("Unsubscribed from events");
}
return true;
}
}
}
Related Pages
- API Reference - Complete method documentation
- Integration Guide - Soft dependency patterns
- Best Practices - Code organization and optimization
- Troubleshooting - Common issues and solutions
Events provide powerful ways to create responsive, dynamic UIs that react to changes in real-time!