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
.
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).
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.
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.
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));
}
}
}