xaf-controllers
XAF: Controllers & Actions
Controller Types
| Type | Base Class | Typical Use |
|---|---|---|
ViewController |
Controller |
Any view; most common base |
ViewController<TView> |
ViewController |
Constrained to specific view type |
ObjectViewController<TView, TObject> |
ViewController<TView> |
View + business object type — no manual casting |
WindowController |
Controller |
Window-level; not tied to a view |
ApplicationController |
Controller |
Global; activates for all views |
Generic shorthand:
// Instead of TargetViewType + TargetObjectType + casting:
public class MyController : ObjectViewController<DetailView, Employee> {
// View is DetailView, ViewCurrentObject is Employee — no cast needed
}
Place controllers in the platform-agnostic Module project (not in Blazor/WinForms projects) unless platform-specific.
Controller Lifecycle
| Method | When Called | Typical Use |
|---|---|---|
OnActivated() |
Controller becomes active | Subscribe to view/object events, set initial state |
OnDeactivated() |
Controller becomes inactive | Unsubscribe events, release resources |
OnViewControlsCreated() |
After all UI controls created | Access and modify native UI controls |
OnViewShown() |
After view shown to user | Logic requiring fully rendered view |
public class MyController : ViewController {
private bool _eventsSubscribed;
protected override void OnActivated() {
base.OnActivated();
if (!_eventsSubscribed) {
View.CurrentObjectChanged += View_CurrentObjectChanged;
View.ObjectSpace.ObjectChanged += ObjectSpace_ObjectChanged;
_eventsSubscribed = true;
}
}
protected override void OnDeactivated() {
Unsubscribe();
base.OnDeactivated();
}
// Always override Dispose — OnDeactivated is not always called before disposal
protected override void Dispose(bool disposing) {
if (disposing) Unsubscribe();
base.Dispose(disposing);
}
private void Unsubscribe() {
if (!_eventsSubscribed) return;
if (View != null) View.CurrentObjectChanged -= View_CurrentObjectChanged;
if (View?.ObjectSpace != null) View.ObjectSpace.ObjectChanged -= ObjectSpace_ObjectChanged;
_eventsSubscribed = false;
}
private void View_CurrentObjectChanged(object sender, EventArgs e) { }
private void ObjectSpace_ObjectChanged(object sender, ObjectChangedEventArgs e) { }
}
Memory leak risk: Not unsubscribing events is the #1 cause of memory leaks in XAF. Always pair
+=inOnActivatedwith-=in bothOnDeactivatedandDispose(bool). Seexaf-memory-leaksfor full patterns includingWeakEventSubscriptionand resource tracker.
Action Types
| Type | Class | UI | Use Case |
|---|---|---|---|
| Simple | SimpleAction |
Button | Trigger an operation |
| Parametrized | ParametrizedAction |
Button + text input | Search, filter by user-typed value |
| Single Choice | SingleChoiceAction |
Dropdown / radio list | Pick from predefined options |
| Popup Window | PopupWindowShowAction |
Button opens modal | Select objects or confirm via popup |
SimpleAction — Full Pattern
public class ClearTasksController : ViewController {
private SimpleAction clearTasksAction;
public ClearTasksController() {
TargetViewType = ViewType.DetailView;
TargetObjectType = typeof(Employee);
clearTasksAction = new SimpleAction(this, "ClearTasksAction", PredefinedCategory.View) {
Caption = "Clear Tasks",
ConfirmationMessage = "Are you sure?",
ImageName = "Action_Clear",
};
clearTasksAction.Execute += ClearTasksAction_Execute;
}
private void ClearTasksAction_Execute(object sender, SimpleActionExecuteEventArgs e) {
var employee = (Employee)View.CurrentObject;
while (employee.DemoTasks.Count > 0)
employee.DemoTasks.Remove(employee.DemoTasks[0]);
View.ObjectSpace.CommitChanges();
View.ObjectSpace.Refresh();
}
}
SimpleAction — Async Pattern (Blazor Critical!)
// CORRECT: async void, no ConfigureAwait(false) in Blazor Server
private async void MyAction_Execute(object sender, SimpleActionExecuteEventArgs e) {
try {
var result = await myService.DoWorkAsync(); // NO ConfigureAwait(false)!
View.ObjectSpace.CommitChanges();
View.Refresh();
Application.ShowViewStrategy.ShowMessage("Success");
}
catch (Exception ex) {
throw new UserFriendlyException($"Error: {ex.Message}");
}
}
// If updating UI from non-Blazor thread, use InvokeAsync:
await InvokeAsync(() => {
View.Refresh();
});
// InvokeAsync is available in BlazorApplication and BlazorController
PopupWindowShowAction — Full Pattern
public class PopupNotesController : ViewController {
private PopupWindowShowAction showNotesAction;
public PopupNotesController() {
TargetObjectType = typeof(DemoTask);
TargetViewType = ViewType.DetailView;
showNotesAction = new PopupWindowShowAction(this, "ShowNotesAction", PredefinedCategory.Edit) {
Caption = "Show Notes",
AcceptButtonCaption = "Select", // custom button captions
CancelButtonCaption = "Back"
};
showNotesAction.CustomizePopupWindowParams += Action_CustomizePopupWindowParams;
showNotesAction.Execute += Action_Execute;
}
private void Action_CustomizePopupWindowParams(
object sender, CustomizePopupWindowParamsEventArgs e) {
// Option 1: show existing objects list
e.View = Application.CreateListView(typeof(Note), true);
// Option 2: show new-object detail view
// IObjectSpace os = Application.CreateObjectSpace(typeof(Note));
// e.View = Application.CreateDetailView(os, os.CreateObject<Note>());
// e.DialogController.SaveOnAccept = true; // save on Accept click
}
private void Action_Execute(
object sender, PopupWindowShowActionExecuteEventArgs e) {
var task = (DemoTask)View.CurrentObject;
foreach (Note note in e.PopupWindowViewSelectedObjects) {
if (!string.IsNullOrEmpty(task.Description))
task.Description += Environment.NewLine;
task.Description += note.Text;
}
View.ObjectSpace.CommitChanges();
}
}
DialogController
DialogController is a WindowController that manages popup windows — provides Accept/Cancel buttons and controls popup lifecycle.
Accessed via CustomizePopupWindowParamsEventArgs.DialogController or Frame.GetController<DialogController>().
Key Properties
| Property | Type | Default | Description |
|---|---|---|---|
AcceptAction |
SimpleAction |
— | The Accept button action; customize caption/active state |
CancelAction |
SimpleAction |
— | The Cancel button action |
SaveOnAccept |
bool |
true |
Save Detail View changes when Accept is clicked |
CanCloseWindow |
bool |
true |
Whether popup closes automatically after Accept/Cancel |
Key Events
| Event | When | Use |
|---|---|---|
Accepting |
Before Accept default behavior | Validate input, set e.Cancel = true to block |
Cancelling |
Before Cancel default behavior | Cleanup before close |
DialogController Customization Example
private void Action_CustomizePopupWindowParams(
object sender, CustomizePopupWindowParamsEventArgs e) {
var os = Application.CreateObjectSpace(typeof(InputObject));
var inputObj = os.CreateObject<InputObject>();
e.View = Application.CreateDetailView(os, inputObj);
// Don't auto-save — handle manually in Execute
e.DialogController.SaveOnAccept = false;
// Customize Accept button
e.DialogController.AcceptAction.Caption = "Confirm";
// Validate before accepting
e.DialogController.Accepting += (s, args) => {
if (string.IsNullOrEmpty(inputObj.Name)) {
args.Cancel = true;
Application.ShowViewStrategy.ShowMessage(
"Name is required", InformationType.Error);
}
};
// Keep window open until explicitly closed
e.DialogController.CanCloseWindow = false;
e.DialogController.Accepting += (s, args) => {
if (IsValid(inputObj)) {
e.DialogController.CanCloseWindow = true;
}
};
}
Access DialogController from Popup's Own Controller
If you place a controller inside the popup view, get DialogController from the Frame:
public class PopupInnerController : ViewController {
protected override void OnActivated() {
base.OnActivated();
// Works only inside a popup window frame
var dialogController = Frame.GetController<DialogController>();
if (dialogController != null) {
dialogController.AcceptAction.Caption = "Apply";
dialogController.Accepting += DialogController_Accepting;
}
}
private void DialogController_Accepting(object sender, DialogControllerAcceptingEventArgs e) {
// Access popup object and validate
var obj = View.CurrentObject as MyObject;
if (obj == null || !obj.IsValid) {
e.Cancel = true;
}
}
}
Chaining Consecutive Popups
To open a second popup after the first one is accepted, trigger the second PopupWindowShowAction.DoExecute() (or show a view manually) inside the first action's Execute handler.
Pattern: Two Sequential Popups
public class ChainedPopupController : ViewController {
private PopupWindowShowAction firstPopupAction;
private PopupWindowShowAction secondPopupAction;
public ChainedPopupController() {
firstPopupAction = new PopupWindowShowAction(this, "FirstPopupAction", PredefinedCategory.Edit) {
Caption = "Step 1: Choose Category"
};
firstPopupAction.CustomizePopupWindowParams += FirstPopup_CustomizeParams;
firstPopupAction.Execute += FirstPopup_Execute;
secondPopupAction = new PopupWindowShowAction(this, "SecondPopupAction", PredefinedCategory.Edit) {
Caption = "Step 2: Choose Item"
};
secondPopupAction.CustomizePopupWindowParams += SecondPopup_CustomizeParams;
secondPopupAction.Execute += SecondPopup_Execute;
// Hide second action from UI — triggered programmatically
secondPopupAction.Active.SetItemValue("Manual", false);
}
private Category _selectedCategory;
private void FirstPopup_CustomizeParams(object sender, CustomizePopupWindowParamsEventArgs e) {
e.View = Application.CreateListView(typeof(Category), true);
}
private void FirstPopup_Execute(object sender, PopupWindowShowActionExecuteEventArgs e) {
_selectedCategory = e.PopupWindowViewCurrentObject as Category;
if (_selectedCategory != null) {
// Open second popup immediately after first closes
secondPopupAction.DoExecute();
}
}
private void SecondPopup_CustomizeParams(object sender, CustomizePopupWindowParamsEventArgs e) {
// Filter items by category selected in first popup
var os = Application.CreateObjectSpace(typeof(Item));
var items = os.GetObjects<Item>(
CriteriaOperator.Parse("Category = ?", _selectedCategory));
e.View = Application.CreateListView(os, Application.FindListViewId(typeof(Item)), true);
}
private void SecondPopup_Execute(object sender, PopupWindowShowActionExecuteEventArgs e) {
var selectedItem = e.PopupWindowViewCurrentObject as Item;
// Use selectedItem — both popups completed
View.ObjectSpace.CommitChanges();
}
}
Pattern: Popup Opened from ShowViewStrategy (manual)
private void FirstPopup_Execute(object sender, PopupWindowShowActionExecuteEventArgs e) {
var firstResult = e.PopupWindowViewCurrentObject as Category;
// Manually show second popup view
var os = Application.CreateObjectSpace(typeof(Item));
var obj = os.CreateObject<Item>();
obj.Category = os.GetObject(firstResult);
var detailView = Application.CreateDetailView(os, obj);
var showParams = new ShowViewParameters(detailView) {
TargetWindow = TargetWindow.NewModalWindow,
Context = TemplateContext.PopupWindow
};
// Add a DialogController to the new popup window
var dc = new DialogController();
dc.SaveOnAccept = true;
showParams.Controllers.Add(dc);
Application.ShowViewStrategy.ShowView(showParams, new ShowViewSource(Frame, null));
}
Important:
secondAction.DoExecute()— works cleanly when usingPopupWindowShowActionTargetWindow.NewModalWindowwithShowViewStrategy.ShowView— for custom views without a pre-configured action- Do NOT call
DoExecute()insideCustomizePopupWindowParams— always inExecute
View Refresh Patterns
ObjectSpace.ObjectChanged — React to Property Changes
Fires when a persistent object's property value changes (tracked via INotifyPropertyChanged).
protected override void OnActivated() {
base.OnActivated();
View.ObjectSpace.ObjectChanged += ObjectSpace_ObjectChanged;
}
private void ObjectSpace_ObjectChanged(object sender, ObjectChangedEventArgs e) {
// e.Object — the modified object
// e.PropertyName — name of changed property (null if indeterminate)
// e.OldValue — previous value (XPO: only if passed to SetPropertyValue/OnChanged)
// e.NewValue — new value (EF Core: requires INotifyPropertyChanging + INotifyPropertyChanged)
if (e.Object is OrderLine line && e.PropertyName == nameof(OrderLine.Quantity)) {
line.TotalPrice = line.Quantity * line.UnitPrice;
}
}
protected override void OnDeactivated() {
View.ObjectSpace.ObjectChanged -= ObjectSpace_ObjectChanged;
base.OnDeactivated();
}
Note: For XPO, OldValue/NewValue are only populated if the model calls SetPropertyValue or OnChanged with explicit old/new values. For EF Core, implement both INotifyPropertyChanging and INotifyPropertyChanged to get those values.
View.CurrentObjectChanged — React to Navigation
Fires when the user navigates to a different record (focused object changes), not when properties change.
protected override void OnActivated() {
base.OnActivated();
View.CurrentObjectChanged += View_CurrentObjectChanged;
}
private void View_CurrentObjectChanged(object sender, EventArgs e) {
var current = View.CurrentObject as Employee;
// Update action state or side-panel based on new current object
myAction.Enabled.SetItemValue("HasObject", current != null);
}
Refresh Reference
| Method | Scope | Use When |
|---|---|---|
ObjectSpace.ReloadObject(obj) |
Single object | Reload one object from DB (e.g., after external change) |
ObjectSpace.Refresh() |
All objects in OS | Full refresh; prompts save if uncommitted changes exist |
View.Refresh() |
UI display only | Redraw view after programmatic data change |
View.ObjectSpace.CommitChanges() + Refresh() |
Commit + reload | Standard post-save pattern |
// Reload single object without affecting whole ObjectSpace
ObjectSpace.ReloadObject(View.CurrentObject);
View.Refresh();
// Full ObjectSpace refresh (may prompt user to save changes)
View.ObjectSpace.Refresh();
// Commit and refresh after action
ObjectSpace.CommitChanges();
View.ObjectSpace.Refresh();
ObjectSpace.ModifiedChanged — React to Dirty State
View.ObjectSpace.ModifiedChanged += (s, e) => {
// ObjectSpace.IsModified changed
saveAction.Enabled.SetItemValue("IsModified", View.ObjectSpace.IsModified);
};
SingleChoiceAction — Full Pattern
public class SetTaskController : ViewController {
private SingleChoiceAction setTaskAction;
public SetTaskController() {
TargetObjectType = typeof(DemoTask);
setTaskAction = new SingleChoiceAction(this, "SetTaskAction", PredefinedCategory.Edit) {
Caption = "Set Task",
ItemType = SingleChoiceActionItemType.ItemIsOperation,
SelectionDependencyType = SelectionDependencyType.RequireMultipleObjects
};
var setPriorityItem = new ChoiceActionItem("Set Priority", null);
setTaskAction.Items.Add(setPriorityItem);
foreach (Priority value in Enum.GetValues(typeof(Priority))) {
setPriorityItem.Items.Add(new ChoiceActionItem(value.ToString(), value));
}
setTaskAction.Execute += SetTaskAction_Execute;
}
private void SetTaskAction_Execute(object sender, SingleChoiceActionExecuteEventArgs e) {
if (e.SelectedChoiceActionItem.Data is Priority priority) {
foreach (DemoTask task in e.SelectedObjects.OfType<DemoTask>())
task.Priority = priority;
View.ObjectSpace.CommitChanges();
View.ObjectSpace.Refresh();
}
}
}
ParametrizedAction
public class FindController : ViewController {
private ParametrizedAction findAction;
public FindController() {
findAction = new ParametrizedAction(this, "FindByName", PredefinedCategory.View, typeof(string)) {
Caption = "Find",
NullValuePrompt = "Enter name..."
};
findAction.Execute += FindAction_Execute;
}
private void FindAction_Execute(object sender, ParametrizedActionExecuteEventArgs e) {
var searchText = e.ParameterCurrentValue as string;
if (string.IsNullOrEmpty(searchText)) return;
var obj = ObjectSpace.FindObject<Contact>(
CriteriaOperator.Parse("Contains([Name], ?)", searchText));
if (obj != null)
View.SelectObject(obj);
}
}
Activation Conditions
// Active: action shown only if all values are true
MyAction.Active.SetItemValue("HasPermission", security.CanCreate(typeof(Order)));
MyAction.Active.SetItemValue("IsCorrectView", View is DetailView);
// Enabled: action visible but grayed out if any value is false
MyAction.Enabled.SetItemValue("HasSelection", View.SelectedObjects.Count > 0);
Frame & NestedFrame
// Access nested frame (e.g., in a MasterDetail view)
if (Frame is NestedFrame nestedFrame) {
var parentController = nestedFrame.ParentFrame
.GetController<ParentViewController>();
}
// Find controller in current frame
var refreshCtrl = Frame.GetController<RefreshController>();
refreshCtrl?.RefreshAction.DoExecute();
Common Patterns
// Show message
Application.ShowViewStrategy.ShowMessage("Operation complete", InformationType.Success);
// Navigate to object detail view
var showViewParams = Application.CreateDetailViewShowViewParameters(obj, objectSpace);
Application.ShowViewStrategy.ShowView(showViewParams, new ShowViewSource(Frame, null));
// Navigate programmatically to ListView
var listView = Application.CreateListView(typeof(Order), true);
Application.ShowViewStrategy.ShowView(
new ShowViewParameters(listView), new ShowViewSource(Frame, null));
// Find objects
var obj = ObjectSpace.FindObject<Contact>(
CriteriaOperator.Parse("Email = ?", "user@example.com"));
var selected = View.SelectedObjects.OfType<Employee>().ToList();
Related Skills
xaf-memory-leaks— full disposal patterns, WeakEventSubscription, ObjectSpace lifetime, diagnostic tools
Source Links
- Controllers: https://docs.devexpress.com/eXpressAppFramework/112623/ui-construction/controllers-and-actions/controllers
- Actions: https://docs.devexpress.com/eXpressAppFramework/112622/ui-construction/controllers-and-actions/actions
- DialogController: https://docs.devexpress.com/eXpressAppFramework/112805/ui-construction/controllers-and-actions/dialog-controller
- DialogController API: https://docs.devexpress.com/eXpressAppFramework/DevExpress.ExpressApp.SystemModule.DialogController
- PopupWindowShowAction How-To: https://docs.devexpress.com/eXpressAppFramework/113539/ui-construction/controllers-and-actions/actions/how-to-create-and-use-a-popup-window-action
- Add Actions to Popup: https://docs.devexpress.com/eXpressAppFramework/112804/ui-construction/controllers-and-actions/add-actions-to-a-popup-window
- ObjectChanged Event: https://docs.devexpress.com/eXpressAppFramework/DevExpress.ExpressApp.IObjectSpace.ObjectChanged
- Execute Logic on Property Change: https://docs.devexpress.com/eXpressAppFramework/403621/data-manipulation-and-business-logic/create-read-update-and-delete-data/execute-business-logic-when-a-property-is-changed-and-track-modifications-in-objects
- Refresh Objects: https://docs.devexpress.com/eXpressAppFramework/403622/data-manipulation-and-business-logic/create-read-update-and-delete-data/refresh-objects-and-rollback-changes
More from kashiash/xaf-skills
xaf
DevExpress XAF (eXpressApp Framework) master index. Use this skill first when working with any XAF topic to find the right sub-skill. Covers Blazor and WinForms, EF Core and XPO, versions v24.2 and v25.1. Sub-skills: xaf-xpo-models, xaf-ef-models, xaf-controllers, xaf-editors, xaf-custom-editors, xaf-nonpersistent, xaf-security, xaf-multi-tenant, xaf-web-api, xaf-validation, xaf-reports, xaf-dashboards, xaf-office, xaf-blazor-ui, xaf-winforms-ui, xaf-conditional-appearance, xaf-deployment, xaf-memory-leaks.
13xaf-winforms-ui
XAF WinForms UI platform - WinApplication setup, Ribbon vs Standard toolbar, WinForms-specific editors (XtraGrid, DevExpress controls), Detail View layout customization via Layout Manager, custom WinForms controls embedded in XAF views, background workers for thread-safe UI updates, splash screen customization, WinForms navigation (NavigationFrame), printing/preview in WinForms, ClickOnce/MSI deployment. Use when building or customizing XAF WinForms applications.
10xaf-blazor-ui
XAF Blazor UI platform - BlazorApplication setup in Program.cs, AddXaf/AddXafBlazor services, InvokeAsync thread safety (critical for Blazor Server), async controller actions pattern, Blazor-specific editors, embedding custom Razor components as ViewItems using IComponentContentHolder, JavaScript interop via IJSRuntime, Detail View layout customization (tabs/groups), programmatic navigation, error handling with UserFriendlyException, SignalR configuration. Use when building or customizing XAF Blazor Server applications.
10xaf-office
XAF Office/Document Management Modules - FileAttachmentsModule with IFileData/FileAttachment patterns (XPO and EF Core), SpreadsheetModule for Excel editing with ISpreadsheetValueStorage, RichTextModule for Word-like editing with IRichTextDocumentProvider and mail merge, PdfViewerModule for PDF display, platform differences (Blazor vs WinForms), programmatic document manipulation. Use when adding file attachments, spreadsheet editing, rich text editing, or PDF viewing to DevExpress XAF applications.
9xaf-reports
XAF Reports Module (XtraReports v2) - ReportsModuleV2 setup for Blazor and WinForms, report storage (DB/filesystem/custom IReportStorageWebExtension), creating predefined reports in code (PredefinedReportsUpdater), data sources (CollectionDataSource/EntityServerModeSource), report parameters, programmatic export (PDF/Excel/Word), in-app designer, PrintAction, security permissions for report design vs view. Use when working with DevExpress XtraReports integration in XAF.
9xaf-editors
XAF built-in property editors and list editors - editor type mapping by data type, EditorAliases constants, [EditorAlias] attribute, [ModelDefault] for DisplayFormat/EditMask, ObjectPropertyEditor for inline sub-forms, list editor types (GridListEditor, DxGridListEditor, TreeListEditor, ChartListEditor), GridListEditor WinForms customization, DxGridListEditor Blazor customization, IModelListView/IModelColumn properties. Use when working with built-in XAF editors or customizing grid/list views.
8