A recent support center question regarding the fact that XPO sends and UPDATE SQL command that includes all fields regardless if they have changed or not, pointed to a rather unhelpful answer.
I too required this functionality and after a bit of trial and error got it all working, so thanks to Martin’s question I will post my answer here ;)
1. BaseObject
My first recommendation when using XPO is to make your own “Base” class (which inherits from one of XPO’s base classes such as XPBaseObject, XPLiteObject, XPCustomObject or XPObject ) which all “YOUR” objects will inherit from.
In our example we will declare a class called BaseObject which will inherit from XPObject.
This will allow us to put common functionality that will apply to all your business classes.
1.1 ChangedMembers
Now we will need a place to store the changed properties as they occur so when it comes to saving we will see what has changed.
Private _ChangedMembers As DevExpress.Xpo.Metadata.Helpers.MemberInfoCollection
Public ReadOnly Property ChangedMembers() As DevExpress.Xpo.Metadata.Helpers.MemberInfoCollection
Get
If _ChangedMembers Is Nothing Then
_ChangedMembers = New DevExpress.Xpo.Metadata.Helpers.MemberInfoCollection(ClassInfo)
End If
Return _ChangedMembers
End Get
End Property
private DevExpress.Xpo.Metadata.Helpers.MemberInfoCollection _ChangedMembers;
public DevExpress.Xpo.Metadata.Helpers.MemberInfoCollection ChangedMembers {
get {
if (_ChangedMembers == null) {
_ChangedMembers = new DevExpress.Xpo.Metadata.Helpers.MemberInfoCollection(ClassInfo);
}
return _ChangedMembers;
}
}
1.2 Overriding OnChanged
Now we have somewhere to store ChangedMembers we need to “monitor” our object and store the changes as they occur. The OnChanged method allows such as task so lets override it.
Protected Overrides Sub OnChanged(ByVal propertyName As String, ByVal oldValue As Object, ByVal newValue As Object)
MyBase.OnChanged(propertyName, oldValue, newValue)
If Not Me.IsLoading Then
Dim Member As DevExpress.Xpo.Metadata.XPMemberInfo = ClassInfo.GetPersistentMember(propertyName)
If Member IsNot Nothing AndAlso Not ChangedMembers.Contains(Member) Then
ChangedMembers.Add(ClassInfo.GetMember(propertyName))
End If
End If
End Sub
protected override void OnChanged(string propertyName, object oldValue, object newValue)
{
base.OnChanged(propertyName, oldValue, newValue);
if (!this.IsLoading) {
DevExpress.Xpo.Metadata.XPMemberInfo Member = ClassInfo.GetPersistentMember(propertyName);
if (Member != null && !ChangedMembers.Contains(Member)) {
ChangedMembers.Add(ClassInfo.GetMember(propertyName));
}
}
}
1.3 Overriding OnSaved
So we have something to hold our changes, mark whats changed and now we need something to say that the changes aren’t changes anymore. We do this with the OnSaved method which is called after the object has been persisted.
Protected Overrides Sub OnSaved()
MyBase.OnSaved()
ChangedMembers.Clear()
End Sub
protected override void OnSaved()
{
base.OnSaved();
ChangedMembers.Clear();
}
1.4 BaseObject Conclusion
So lets see the full BaseObject class now
Imports System
Imports DevExpress.Xpo
<NonPersistent()> _
Public Class BaseObject
Inherits XPObject
Public Sub New(ByVal session As Session)
MyBase.New(session)
' This constructor is used when an object is loaded from a persistent storage.
' Do not place any code here.
End Sub
Public Overrides Sub AfterConstruction()
MyBase.AfterConstruction()
' Place here your initialization code.
End Sub
Private _ChangedMembers As DevExpress.Xpo.Metadata.Helpers.MemberInfoCollection
Public ReadOnly Property ChangedMembers() As DevExpress.Xpo.Metadata.Helpers.MemberInfoCollection
Get
If _ChangedMembers Is Nothing Then
_ChangedMembers = New DevExpress.Xpo.Metadata.Helpers.MemberInfoCollection(ClassInfo)
End If
Return _ChangedMembers
End Get
End Property
Protected Overrides Sub OnChanged(ByVal propertyName As String, ByVal oldValue As Object, ByVal newValue As Object)
MyBase.OnChanged(propertyName, oldValue, newValue)
If Not Me.IsLoading Then
Dim Member As DevExpress.Xpo.Metadata.XPMemberInfo = ClassInfo.GetPersistentMember(propertyName)
If Member IsNot Nothing AndAlso Not ChangedMembers.Contains(Member) Then
ChangedMembers.Add(ClassInfo.GetMember(propertyName))
End If
End If
End Sub
Protected Overrides Sub OnSaved()
MyBase.OnSaved()
ChangedMembers.Clear()
End Sub
End Class
using System;
using DevExpress.Xpo;
[NonPersistent()]
public class BaseObject : XPObject
{
public BaseObject(Session session) : base(session)
{
// This constructor is used when an object is loaded from a persistent storage.
// Do not place any code here.
}
public override void AfterConstruction()
{
// Place here your initialization code.
base.AfterConstruction();
}
private DevExpress.Xpo.Metadata.Helpers.MemberInfoCollection _ChangedMembers;
public DevExpress.Xpo.Metadata.Helpers.MemberInfoCollection ChangedMembers {
get {
if (_ChangedMembers == null) {
_ChangedMembers = new DevExpress.Xpo.Metadata.Helpers.MemberInfoCollection(ClassInfo);
}
return _ChangedMembers;
}
}
protected override void OnChanged(string propertyName, object oldValue, object newValue)
{
base.OnChanged(propertyName, oldValue, newValue);
if (!this.IsLoading) {
DevExpress.Xpo.Metadata.XPMemberInfo Member = ClassInfo.GetPersistentMember(propertyName);
if (Member != null && !ChangedMembers.Contains(Member)) {
ChangedMembers.Add(ClassInfo.GetMember(propertyName));
}
}
}
protected override void OnSaved()
{
base.OnSaved();
ChangedMembers.Clear();
}
}
2. XtraUnitOfWork
Now we have a place to store our ChangedMembers and our object is “identifying” which members are changing. Now we need tell the “DataLayer” only about these changes. Our UnitOfWork acheives this by overriding the GetPropertiesListForUpdateInsert method (Technically you can Inherit from Session, but as per XPO Best Practices use of Session isn’t “recommended”).
Imports DevExpress.Xpo
Imports DevExpress.Data.Filtering
Public Class XtraUnitOfWork
Inherits UnitOfWork
Public Sub New(ByVal dictionary As Metadata.XPDictionary)
MyBase.New(dictionary)
End Sub
Public Sub New(ByVal layer As IDataLayer, ByVal ParamArray DisposeOnDisconnect() As IDisposable)
MyBase.New(layer, DisposeOnDisconnect)
End Sub
Public Sub New()
MyBase.New()
End Sub
Protected Overrides Function GetPropertiesListForUpdateInsert(ByVal theObject As Object, ByVal isUpdate As Boolean) As Metadata.Helpers.MemberInfoCollection
'Check if the Object is our "change tracking object"
If TypeOf theObject Is BaseObject Then
Dim ci As Metadata.XPClassInfo = GetClassInfo(theObject) 'obtain the class info so we can walk through the members
Dim list As Metadata.Helpers.MemberInfoCollection = New Metadata.Helpers.MemberInfoCollection(ci) 'obtain a list of members
Dim count As Integer = 0
For Each m As Metadata.XPMemberInfo In MyBase.GetPropertiesListForUpdateInsert(theObject, isUpdate)
'If it is a servicefield this is required (OID, GCRecord etc)
If TypeOf m Is Metadata.Helpers.ServiceField Or CType(theObject, BaseObject).ChangedMembers.Contains(m) Then
list.Add(m)
End If
Next
'return out list of "changed" members (plus the service fields)
Return list
Else
Return MyBase.GetPropertiesListForUpdateInsert(theObject, isUpdate)
End If
End Function
End Class
using DevExpress.Xpo;
using DevExpress.Data.Filtering;
public class XtraUnitOfWork : UnitOfWork
{
public XtraUnitOfWork(Metadata.XPDictionary dictionary) : base(dictionary)
{
}
public XtraUnitOfWork(IDataLayer layer, params IDisposable[] DisposeOnDisconnect) : base(layer, DisposeOnDisconnect)
{
}
public XtraUnitOfWork() : base()
{
}
protected override Metadata.Helpers.MemberInfoCollection GetPropertiesListForUpdateInsert(object theObject, bool isUpdate)
{
//Check if the Object is our "change tracking object"
if (theObject is BaseObject) {
Metadata.XPClassInfo ci = GetClassInfo(theObject);
//obtain the class info so we can walk through the members
Metadata.Helpers.MemberInfoCollection list = new Metadata.Helpers.MemberInfoCollection(ci);
//obtain a list of members
int count = 0;
foreach (Metadata.XPMemberInfo m in base.GetPropertiesListForUpdateInsert(theObject, isUpdate)) {
//If it is a servicefield this is required (OID, GCRecord etc)
if (m is Metadata.Helpers.ServiceField | ((BaseObject)theObject).ChangedMembers.Contains(m)) {
list.Add(m);
}
}
//return out list of "changed" members (plus the service fields)
return list;
}
else {
return base.GetPropertiesListForUpdateInsert(theObject, isUpdate);
}
}
}
3. Customer Class
Lets just create a quick test class, we’ll call it Customer
Imports System
Imports DevExpress.Xpo
Public Class Customer
Inherits BaseObject
Public Sub New(ByVal session As Session)
MyBase.New(session)
' This constructor is used when an object is loaded from a persistent storage.
' Do not place any code here.
End Sub
Public Overrides Sub AfterConstruction()
MyBase.AfterConstruction()
' Place here your initialization code.
End Sub
Private _company As String
<Size(254)> _
Public Property Company() As String
Get
Return _company
End Get
Set(ByVal Value As String)
SetPropertyValue("Company", _company, Value)
End Set
End Property
Private _webAddress As String
<Size(254)> _
Public Property WebAddress() As String
Get
Return _webAddress
End Get
Set(ByVal Value As String)
SetPropertyValue("WebAddress", _webAddress, Value)
End Set
End Property
Private _notes As String
<Size(SizeAttribute.Unlimited)> _
Public Property Notes() As String
Get
Return _notes
End Get
Set(ByVal Value As String)
SetPropertyValue("Notes", _notes, Value)
End Set
End Property
End Class
using System;
using DevExpress.Xpo;
public class Customer : BaseObject
{
public Customer(Session session) : base(session)
{
// This constructor is used when an object is loaded from a persistent storage.
// Do not place any code here.
}
public override void AfterConstruction()
{
// Place here your initialization code.
base.AfterConstruction();
}
private string _company;
[Size(254)]
public string Company {
get { return _company; }
set { SetPropertyValue("Company", _company, value); }
}
private string _webAddress;
[Size(254)]
public string WebAddress {
get { return _webAddress; }
set { SetPropertyValue("WebAddress", _webAddress, value); }
}
private string _notes;
[Size(SizeAttribute.Unlimited)]
public string Notes {
get { return _notes; }
set { SetPropertyValue("Notes", _notes, value); }
}
}
Which is when we look at a the diagram we can see it derives from BaseObject which derivces from XPObject

4. Test it out
Setup your XPO tracing (http://www.devexpress.com/Support/Center/kb/p/A2572.aspx) and open an object change one field and CommitChanges on your XtraUnitOfWork, you should find only the change is persisted.
Here is a complete demo of the above classes
DOWNLOAD HERE
5. NestedUnitOfWork Consideration
I did miss out some extra information above as I wanted you to understand how it all works before having to think about even more stuff ;)
When we override the OnSaved() method and clear the ChangedMembers collection, we have to take into account another awesome feature of XPO, NestedUnitOfWork.
Basically you can “CommitChanges” on a NestedUnitOfWork and at this stage it hasn’t actually “persisted’ the data to the database, but rather “persisted’ the changes to the “parent” UnitOfWork.
So to make sure that we “track” the changes we need to add this code before the ChangedMembers.Clear() line
So now the OnSaved() method should look like this
Protected Overrides Sub OnSaved()
MyBase.OnSaved()
If TypeOf Session Is NestedUnitOfWork Then
Dim parentitem As BaseObject = CType(Session, NestedUnitOfWork).GetParentObject(Me)
For Each ChangedProperty As DevExpress.Xpo.Metadata.XPMemberInfo In ChangedMembers
If Not parentitem.ChangedMembers.Contains(ChangedProperty) Then
parentitem.ChangedMembers.Add(ChangedProperty)
End If
Next
End If
ChangedMembers.Clear()
End Sub
protected override void OnSaved()
{
base.OnSaved();
if (Session is NestedUnitOfWork) {
BaseObject parentitem = ((NestedUnitOfWork)Session).GetParentObject(this);
foreach (DevExpress.Xpo.Metadata.XPMemberInfo ChangedProperty in ChangedMembers) {
if (!parentitem.ChangedMembers.Contains(ChangedProperty)) {
parentitem.ChangedMembers.Add(ChangedProperty);
}
}
}
ChangedMembers.Clear();
}
I have included this code already in the demo.
Add comment
Comments
Thanks for stopping by, glad it was helpful.
Regards, Michael
Also, the C# versions of 1.2, 1.3 shows VB.
Thanks for sharing this.
Regards,
Mohsen
I understand what you mean about bandwidth usage, my app is used over .NET remoting over ADSL connections so every KB counts.
In my application (not XAF) I use XPView instead of XPCollections and display readonly lists to users to which a detail form is required to edit, the advantage here is you only get the columns you require, the other advantage here is with "Related" fields such as Status and Type etc, with the XPView I return what ever relation field I want like Status.Title, whereas with XPCollection it returns you main object then had to grab the Status object as well to give you Status.Title which I find massively excessive.
In my app, I have it so when I go to load the XPView I obtain all the fieldsnames from my XtraGrid Columns that are visible and only add those columns to my XPView, this means if the user only has 5 columns on the grid I receive 5 fields, if they had all 50 of em they download all 50.
Unfortunately XAF doesn't support XPViews, it relies on the use of ServerMode, which in my case wasn't possible as I am connected to a legacy Access database.
Hopefully this helps out a little for you.
I just looked at BaseObject class of XAF and quite surprisingly, it didn't implement this feature too.
I agree it seems like a "normal" thing to do, why persist values that didn't change, however please following this forum thread http://community.devexpress.com/forums/t/87479.aspx
In there Gary does point out that your would have to watch out with your Concurrency, as you could end up with 2 people with 2 incorrect views of the data (but this is only if you use XPLiteObject without OptimisticLocki ng)
I suppose it comes down to "lowest" common denominator, the big plus is that XPO gives us the functionality to extend it and make it do what we want ;)
Please also look at the forum posts regarding how to ensure this works with Persisted ReadOnly fields (PersistentAlia s) there is an update, unfortunately I haven't updated this post yet.
Also near the end of the post is an IMPORTANT NOTE regarding New Objects and default values, I would suggest performing a check and if the object is new to persist the entire object and not just changed (again code is on that forum thread and I will update this post at some stage)
Thanks for popping by
True, but it's worth noting that DX do reserve the right to change their API from time to time (for example, http://www.devexpress.com/Support/Center/p/B142652.aspx), so it's worth considering whether you can do what you need with XPO "out of the box" before extending.
Very true ;) however keep in mind that this isn't hacking as such :)
GetPropertyList ForUpdateInsert was made virtual for this purpose and, there is nothing stopping DX adding this functionality into the PersistentBase and Session classes, and our "implementations " wouldn't be effected, however could finally be removed and just use the base implementation.
I welcome DX to include this functionality within the suite, and infact just had a look through and the only mention is the original support ticket which made this function available.
http://www.devexpress.com/Support/Center/p/AS5031.aspx
So I have just made the suggestion again
http://www.devexpress.com/issue=S134356
Regards,
Michael Proctor
Keep in mind that DC is "another" way you can have persistant objects, your current PersistantClass es will still work with XAF, just now XAF also supports Persistant Interfaces as well.
So ultimately you won't have a problem immediately, only if you move across to DC technology.
Will ask Gary the question ;)
RSS feed for comments to this post.