skills/kashiash/xaf-skills/xaf-nonpersistent

xaf-nonpersistent

SKILL.md

XAF: NonPersistent Objects

What Are NonPersistent Objects

NonPersistent objects are business classes for which XAF generates full UI (List Views, Detail Views, Actions) but which are not mapped to any database table. They hold data loaded from external sources, computed at runtime, or used as temporary input forms.

  • Decorated with [DomainComponent] attribute (NOT [NonPersistent] from XPO — different meaning!)
  • Do NOT inherit from XPO persistent base classes (causes SessionMixingException)
  • Managed by NonPersistentObjectSpace
  • Must register .AddNonPersistent() in app builder

Use Cases

  • Display data from external REST APIs / stored procedures
  • Intermediate input forms (collect parameters before executing an action)
  • Computed aggregates / dashboards with no DB binding
  • PopupWindowShowAction dialogs for user input
  • Settings/configuration screens without persistence

Base Classes

Class IObjectSpaceLink Auto Key Notes
NonPersistentBaseObject Yes Yes (Guid Oid) Recommended default
NonPersistentLiteObject No Yes Lightweight
NonPersistentObjectImpl Yes No Manual key

Basic NonPersistent Object

using DevExpress.ExpressApp;
using DevExpress.ExpressApp.DC;

[DomainComponent]
public class ContactSummary : NonPersistentBaseObject {
    private string name;
    private string email;
    private int orderCount;

    public string Name {
        get => name;
        set => SetPropertyValue(ref name, value);
    }

    public string Email {
        get => email;
        set => SetPropertyValue(ref email, value);
    }

    public int OrderCount {
        get => orderCount;
        set => SetPropertyValue(ref orderCount, value);
    }
}

SetPropertyValue (from NonPersistentBaseObject) triggers INotifyPropertyChanged automatically.

For Blazor, always declare an explicit key:

[Browsable(false)]
[DevExpress.ExpressApp.Data.Key]
public Guid Oid { get; set; } = Guid.NewGuid();

Registration

// Startup.cs / Program.cs — MUST be after persistent providers
builder.ObjectSpaceProviders
    .AddSecuredEFCoreObjectSpaceProvider<MyDbContext>(...)  // persistent first
    .AddNonPersistent();                                     // non-persistent last

The type must be in the module's ExportedTypes or declared via [DomainComponent].


ObjectsGetting Event — Providing Data for List Views

Primary pattern for populating non-persistent collections:

public class ContactSummaryController : WindowController {
    protected override void OnActivated() {
        base.OnActivated();
        Application.ObjectSpaceCreated += Application_ObjectSpaceCreated;
    }

    protected override void OnDeactivated() {
        Application.ObjectSpaceCreated -= Application_ObjectSpaceCreated;
        base.OnDeactivated();
    }

    private void Application_ObjectSpaceCreated(object sender, ObjectSpaceCreatedEventArgs e) {
        if (e.ObjectSpace is NonPersistentObjectSpace npos) {
            npos.ObjectsGetting += NonPersistentObjectSpace_ObjectsGetting;
        }
    }

    private void NonPersistentObjectSpace_ObjectsGetting(object sender, ObjectsGettingEventArgs e) {
        if (e.ObjectType != typeof(ContactSummary)) return;

        var npos = (NonPersistentObjectSpace)sender;
        var items = LoadDataFromApi(); // IList<ContactSummary>

        var collection = new DynamicCollection(npos, e.ObjectType, e.Criteria, e.Sorting, e.InTransaction);
        collection.FetchObjects += (s, args) => {
            args.Objects = items;
            args.ShapeData = true; // XAF applies criteria/sorting to the list
        };
        e.Objects = collection;
    }

    private IList<ContactSummary> LoadDataFromApi() {
        return new List<ContactSummary> {
            new ContactSummary { Name = "Alice", Email = "alice@example.com", OrderCount = 5 },
            new ContactSummary { Name = "Bob",   Email = "bob@example.com",   OrderCount = 2 }
        };
    }
}

ShapeData = true automatically applies e.Criteria and e.Sorting to your list.


ObjectByKeyGetting — Single Object Fetch

Used when XAF needs to retrieve a single object by key (e.g., navigation to Detail View):

npos.ObjectByKeyGetting += (sender, e) => {
    if (e.ObjectType != typeof(ContactSummary)) return;
    var key = (Guid)e.Key;
    e.Object = FetchSingleContact(key);
};

ObjectDeleting / ObjectsDeleting

npos.ObjectDeleting += (sender, e) => {
    if (e.Object is ContactSummary summary)
        ExternalCache.Remove(summary.Oid);
};

npos.ObjectsDeleting += (sender, e) => {
    foreach (ContactSummary obj in e.Objects.OfType<ContactSummary>())
        ExternalCache.Remove(obj.Oid);
};

Using with PopupWindowShowAction

Classic pattern: show non-persistent form as popup to collect user input, then act on result:

public class SendEmailController : ObjectViewController<ListView, Contact> {
    private PopupWindowShowAction sendEmailAction;

    public SendEmailController() {
        sendEmailAction = new PopupWindowShowAction(this, "SendEmail", "Edit") {
            Caption = "Send Email"
        };
        sendEmailAction.CustomizePopupWindowParams += Action_CustomizePopupWindowParams;
        sendEmailAction.Execute += Action_Execute;
    }

    private void Action_CustomizePopupWindowParams(
        object sender, CustomizePopupWindowParamsEventArgs e) {
        var os = Application.CreateObjectSpace(typeof(EmailParams));
        var emailParams = os.CreateObject<EmailParams>();

        // Pre-fill from current selection
        emailParams.To = View.SelectedObjects.OfType<Contact>()
            .Select(c => c.Email).FirstOrDefault();

        // Suppress "save changes" dialog
        ((NonPersistentObjectSpace)os).RemoveFromModifiedObjects(emailParams);

        e.View = Application.CreateDetailView(os, emailParams);
    }

    private void Action_Execute(object sender, PopupWindowShowActionExecuteEventArgs e) {
        var emailParams = (EmailParams)e.PopupWindowViewCurrentObject;
        EmailService.Send(emailParams.To, emailParams.Subject, emailParams.Body);
    }
}

[DomainComponent]
public class EmailParams : NonPersistentBaseObject {
    private string to, subject, body;
    public string To      { get => to;      set => SetPropertyValue(ref to, value); }
    public string Subject { get => subject; set => SetPropertyValue(ref subject, value); }
    [FieldSize(FieldSizeAttribute.Unlimited)]
    public string Body    { get => body;    set => SetPropertyValue(ref body, value); }
}

Relations with Persistent Objects

[DomainComponent]
public class OrderReport : NonPersistentBaseObject {
    // Reference to persistent Customer — load via nested IObjectSpace
    public Customer Customer { get; set; }
}

// In ObjectsGetting:
var persistentOs = Application.CreateObjectSpace(typeof(Customer));
var customer = persistentOs.GetObjectByKey<Customer>(customerId);
// assign to non-persistent object's property

Refreshing NonPersistent Data

// From a controller action — fires ObjectsGetting again
View.ObjectSpace.Refresh();

// Or via built-in Refresh action:
Frame.GetController<RefreshController>().RefreshAction.DoExecute();

Suppressing "Save Changes" Dialog

var os = Application.CreateObjectSpace(typeof(EmailParams));
var obj = os.CreateObject<EmailParams>();
// ... populate obj ...
((NonPersistentObjectSpace)os).RemoveFromModifiedObjects(obj);
e.View = Application.CreateDetailView(os, obj);

Common Pitfalls

Pitfall Solution
Inheriting XPO base class + [DomainComponent] Use only [DomainComponent] on a POCO
Missing explicit [Key] in Blazor Blazor requires [DevExpress.ExpressApp.Data.Key] property
NonPersistent as FIRST object space provider Always register after persistent providers
ObjectsGetting not firing Check AddNonPersistent() is registered and type is in ExportedTypes
Unexpected save dialog Call RemoveFromModifiedObjects() after programmatic creation
Collections of persistent objects mixing object spaces Load them via nested IObjectSpace from the parent

Source Links

Weekly Installs
4
GitHub Stars
2
First Seen
4 days ago
Installed on
opencode4
claude-code4
github-copilot4
codex4
kimi-cli4
amp4