Skip to main content
.NET Framework 4.5.2+

Relationships Between Persistent Objects in Code and UI

  • 6 minutes to read

When you design a business model, you may need to specify relationships between business objects. This topic describes how to set these relationships between persistent objects in an XPO application and demonstrates how XAF UI displays these relationships.

Tip

To learn about relationships between entities in EF Core, refer to the following help topic: Relationships Between Entities in Code and UI (EF Core)

The “Many” side of an association is a collection property and XAF UI uses ListPropertyEditor to display it in WinForms and ASP.NET Web Forms applications.

The “One” side of an association is a reference property. To display it, XAF uses LookupPropertyEditor and ASPxLookupPropertyEditor in WinForms and ASP.NET Web Forms applications, respectively.

If you apply an ExpandObjectMembersAttribute to a reference property and use the ExpandObjectMembers.Never parameter, XAF uses ObjectPropertyEditor instead.

Each object collection has associated Actions that depend on the collection type. You can use the Model Editor to specify whether New, Delete, Link, and Unlink Actions are visible. Set the List View’s AllowNew, AllowDelete, AllowLink, or AllowUnlink property to false to hide these Actions.

If a child object is aggregated (for example, the object is a part of a master object and has an AggregatedAttribute), XAF creates an XPNestedObjectSpace Object Space for the object’s Detail View, because this object should not be physically saved to the database until its owner is saved. If a child object is not aggregated (for example, it can exist separately), XAF uses a separate XPObjectSpace for the object’s Detail View. A nested List View that shows objects from the Detail collections uses the same master XPObjectSpace regardless of the aggregation.

This topic includes the following sections.

One-to-Many (Non-Aggregated)

The relationship between Department and Contacts illustrates the One-to-Many relationship type. Many Contacts can be included in one Department. In this example, the Department object is the “One” side of the relationship. It contains a child ContactsCollection.

NonAggregatedOneToMany_Win

NonAggregatedOneToMany_Web

The List View that displays the ContactsCollection includes a New Action. This Action allows users to add new Contact objects to one of the existing Department objects (including the current object). In addition, the Link and Unlink Actions are available, and allow you to add and remove a reference to a Contact object from another collection.

XAF creates non-aggregated objects in their own Object Space, therefore you can save the new Contact objects created in the Department Detail View separately from the parent object.

The following code snippet demonstrates how to implement this type of a relationship:

[DefaultClassOptions]
public class Contact : XPObject {
    //...
    private Department department;
    [Association("Department-Contacts")]
    public Department Department {
        get { return department; }
        set { SetPropertyValue(nameof(Department), ref department, value); }
    }
}
[DefaultClassOptions]
public class Department : XPObject {
    //...
    [Association("Department-Contacts")]
    public XPCollection<Contact> ContactsCollection {
        get { return GetCollection<Contact>(nameof(ContactsCollection)); }
    }
}

One-to-Many (Aggregated)

A Contact has a collection of Notes that are aggregated with their parent. In this case, the Note object references the “One” aggregated side of the One-to-Many relationship (the Contact object).

AggregatedOneToMany_Win

AggregatedOneToMany_Web

The List View that displays the NotesCollection includes the New Action. This Action allows users to add new Note objects. In this case, the Contact property of the new Note objects is automatically set to the current Contact and the UI does not display this editor.

XAF creates aggregated objects in the parent object’s Object Space. The new Note objects created within the Contact Detail View are saved or deleted when their parent object is saved or deleted (cascade deletion mechanism).

The following code snippet demonstrates how to implement this type of a relationship:

[DefaultClassOptions]
public class Contact : XPObject {
    //...
    [Association("Contact-Notes"), DevExpress.Xpo.Aggregated]
    public XPCollection<Note> NotesCollection {
        get { return GetCollection<Note>(nameof(NotesCollection)); }
    }
}
[DefaultClassOptions]
public class Note : XPObject {
    //...
    private Contact contact;
    [Association("Contact-Notes")]
    public Contact Contact {
        get { return contact; }
        set { SetPropertyValue(nameof(Contact), ref contact, value); }
    }
}

Many-to-Many

Each Contact can have a collection of Tasks and each Task can be assigned to a number of Contacts. Thus, the relationship between the Contact and Task objects is named Many-to-Many.

ManyToManyObject_Win

ManyToManyObject_Web

The List View that displays the TasksCollection includes the Link Action. This Action allows users to add references to existing Task objects. If the NewObjectViewController.LinkNewObjectToParentImmediately property is set to true, the New Action is not applied to this collection. This behavior complies with the unique concept of the Many-to-Many relationship. However, you can create a new Task in the Link Action’s pop-up window.

The Unlink Action is also available for the TasksCollection. This Action allows users to remove references to Task objects from the collection.

The following code snippet demonstrates how to implement this type of relationship:

Technique 1

[DefaultClassOptions]
public class Contact : XPObject {
    //...
    [Association("Contacts-Tasks")]
    public XPCollection<Task> TasksCollection {
        get { return GetCollection<Task>(nameof(TasksCollection)); }
    }
}
[DefaultClassOptions]
public class Task : XPObject {
    //...
    [Association("Contacts-Tasks")]
    public XPCollection<Contact> ContactsCollection {
        get { return GetCollection<Contact>(nameof(ContactsCollection)); }
    }
}

Technique 2 (with an Intermediate Object)

using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base;
using DevExpress.Xpo;
using System.Collections.Generic;
using System.ComponentModel;
// ...
[DefaultClassOptions]
public class Contact : XPObject {
    // ...
    [Browsable(false)]
    [Association("Contact-ContactTasks"), Aggregated]
    public XPCollection<ContactTask> ContactTasks { 
        get { return GetCollection<ContactTask>(nameof(ContactTasks)); } }
    [ManyToManyAlias(nameof(ContactTasks), nameof(ContactTask.Task))]
    public IList<Task> TaskCollection {
        get { return GetList<Task>(nameof(TaskCollection)); }
    }
}
[DefaultClassOptions]
public class Task : XPObject {
    // ...
    [Browsable(false)]
    [Association("Task-ContactTasks"), Aggregated]
    public XPCollection<ContactTask> ContactTasks { 
        get { return GetCollection<ContactTask>(nameof(ContactTasks)); } }
    [ManyToManyAlias(nameof(ContactTasks), nameof(ContactTask.Contact))]
    public IList<Contact> ContactCollection {
        get { return GetList<Contact>(nameof(ContactCollection)); }
    }
}
// Uncomment the following line if your application uses the Security System. 
// [IntermediateObject(nameof(Contact), nameof(Task))]
public class ContactTask : XPObject {
    public ContactTask(Session session) : base(session) { }
    Contact fContact;
    [Association("Contact-ContactTasks")]
    public Contact Contact {
        get { return fContact; }
        set { SetPropertyValue<Contact>(nameof(Contact), ref fContact, value); }
    }
    Task fTask;
    [Association("Task-ContactTasks")]
    public Task Task {
        get { return fTask; }
        set { SetPropertyValue<Task>(nameof(Task), ref fTask, value); }
    }
}

For more information on this technique, refer to the following help topic: Relationships Between Objects.

One-to-One

Each Contact can have only one associated Address and the same Address cannot be assigned to many Contacts. The relationship is One-to-One.

OneToOneObject_Win

OneToOneObject_Web

This relationship doesn’t include a collection side. In this case, the Contact property of the new Address objects is automatically set to the current Contact.

The following code snippet demonstrates how to implement this type of a relationship:

[DefaultClassOptions]
public class Contact : XPObject {
    //...
    [Aggregated]
    private Address address;
    public Address Address{
        get { return address; }
        set {
            if (address == value) return;
            Address prevAddress = address;
            address = value;
            if (IsLoading) return;
            if (prevAddress!= null && prevAddress.Contact == this)
                prevAddress.Contact = null;
            if (address != null)
                address.Contact = this;
            OnChanged(nameof(Address));
        }
    }
}
[DefaultClassOptions]
public class Address : XPObject {
    //...
    Contact contact = null;
    public Contact Contact {
        get { return contact; }
        set {
            if (contact == value)
                return;
            Contact prevContact = contact;
            contact = value;
            if (IsLoading) return;
            if (prevContact != null && prevContact.Address == this)
                prevContact.Address = null;
            if (contact != null)
                contact.Address = this;
            OnChanged(nameof(Contact));
        }
    }
}
See Also