SBD.GL General Ledger: Part 1 A self referencing class business object

xaf
working-out-loud
#1

Source code is On GitHub

Realizing that the Cashbook project is looking more like a general ledger, I am started over again.
XAF->Windows->Entiy Framework Code First.
This time I check

  • Pivot Chart ( with show additional navigation)
  • Pivot Grid ( that looks good)
  • Reports with Enable in-place reports and Show additional navigation items for in-place reports
  • Tree List Editors
  • Validation

I did not use security as I plan to convert this to a UWP app later on, so the db will be on the laptop.

Next I copied over the business classes from SBD.Cashbook.

#2

I am add the following to the DBContext to help prevent accidental deletions by the user.


        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
            base.OnModelCreating(modelBuilder);
        }

#3

Investigating making the accounts use the tree view

https://documentation.devexpress.com/eXpressAppFramework/112836/Concepts/Extra-Modules/TreeList-Editors-Module/TreeList-Editors-Module-Overview

Hmm the doc mentions EF needs to implement IHCategory

Here is my first try

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DevExpress.ExpressApp.DC;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.Base.General;
using DevExpress.Persistent.Validation;

namespace SBD.GL.Module.BusinessObjects
{
    [NavigationItem("Main")]
    [DefaultProperty("Code")]
    [VisibleInReports]
   
    public class Account : BasicBo, IHCategory
    {
        public Account()
        {
            Children = new BindingList<Account>();
        }


        [Browsable(false)] public int Category { get; set; }

        [Browsable(true)]
        [VisibleInDetailView(true)]
        public virtual Account Parent { get; set; }

        [InverseProperty("Parent")]
        [Aggregated]
        public virtual BindingList<Account> Children { get; set; }


        [MaxLength(60)] // so we can index it
        [System.ComponentModel.DataAnnotations.Schema.Index(IsUnique = true)]
        public string Code { get; set; }

        public string Notes { get; set; }

        public decimal OpeningBalance { get; set; }

        [NotMapped]
        public GLCategory GlCategory
        {
            get => (GLCategory) Category;
            set => Category = (int) value;
        }

        [Browsable(false)]
        [RuleFromBoolProperty("ParentCategoryOk", DefaultContexts.Save, "Parent Category if present must match")]
        public bool ParentCategoryOk
        {
            get
            {
                if (Parent == null) return true;
                return Parent.Category == Category;
            }
        }

        ITreeNode ITreeNode.Parent => Parent;
        string ITreeNode.Name => Code;

        ITreeNode IHCategory.Parent
        {
            get => Parent;
            set => Parent = (Account) value;
        }

        string IHCategory.Name
        {
            get => Code;
            set => Code = value;
        }

        IBindingList ITreeNode.Children => Children;
    }
}

I was able to add the first record and a child record
but when I try to add the second child record I run into problems

I wonder why it lets me add one child record but not the next.
OK to trap the error code…
Ctrl Alt E

Check Common Language Runtime Exceptions

image

Start in the debugger
This old one is the first issue

I know to just uncheck Break when this exception type is thrown.

#4

Now the next break shows the exception I am interested in

Exception thrown: 'System.InvalidOperationException' in EntityFramework.dll
Additional information: Multiplicity constraint violated. 
The role 'Account_Children_Source' of the relationship 
'SBD.GL.Module.BusinessObjects.Account_Children' has multiplicity 1 or 0..1. occurred

The call stack is

 	EntityFramework.dll!System.Data.Entity.Core.Objects.EntityEntry.AddDetectedRelationship<System.Data.Entity.Core.Objects.Internal.IEntityWrapper>(System.Collections.Generic.Dictionary<System.Data.Entity.Core.Objects.Internal.IEntityWrapper,System.Collections.Generic.Dictionary<System.Data.Entity.Core.Objects.DataClasses.RelatedEnd,System.Collections.Generic.HashSet<System.Data.Entity.Core.Objects.Internal.IEntityWrapper>>> relationships, System.Data.Entity.Core.Objects.Internal.IEntityWrapper relatedObject, System.Data.Entity.Core.Objects.DataClasses.RelatedEnd relatedEnd) + 0x166 bytes	
 	EntityFramework.dll!System.Data.Entity.Core.Objects.EntityEntry.AddRelationshipDetectedByGraph(System.Collections.Generic.Dictionary<System.Data.Entity.Core.Objects.Internal.IEntityWrapper,System.Collections.Generic.Dictionary<System.Data.Entity.Core.Objects.DataClasses.RelatedEnd,System.Collections.Generic.HashSet<System.Data.Entity.Core.Objects.Internal.IEntityWrapper>>> relationships, object relatedObject, System.Data.Entity.Core.Objects.DataClasses.RelatedEnd relatedEndFrom, bool verifyForAdd) + 0xb6 bytes	
 	EntityFramework.dll!System.Data.Entity.Core.Objects.EntityEntry.DetectChangesInRelationshipsOfSingleEntity() + 0x2a6 bytes	
 	EntityFramework.dll!System.Data.Entity.Core.Objects.ObjectStateManager.DetectChangesInNavigationProperties(System.Collections.Generic.IList<System.Data.Entity.Core.Objects.EntityEntry> entries) + 0x4e bytes	
 	EntityFramework.dll!System.Data.Entity.Core.Objects.ObjectStateManager.DetectChanges() + 0x52 bytes	
 	EntityFramework.dll!System.Data.Entity.Core.Objects.ObjectContext.DetectChanges() + 0x14 bytes	
 	DevExpress.ExpressApp.EF.v18.2.dll!DevExpress.ExpressApp.EF.EFObjectSpace.DetectChanges() Line 87 + 0xb bytes	C#
 	DevExpress.ExpressApp.EF.v18.2.dll!DevExpress.ExpressApp.EF.EFObjectSpace.GetModifiedObjects() Line 93	C#
 	DevExpress.ExpressApp.EF.v18.2.dll!DevExpress.ExpressApp.EF.EFObjectSpace.ModifiedObjects.get() Line 440 + 0x6 bytes	C#
 	DevExpress.ExpressApp.Win.v18.2.dll!DevExpress.ExpressApp.Win.SystemModule.CustomCollectModifiedObjectsEventArgs.CollectModifiedObjects(bool checkObjectSpaceIsModified) Line 150 + 0x15 bytes	C#
 	DevExpress.ExpressApp.Win.v18.2.dll!DevExpress.ExpressApp.Win.SystemModule.LockController.GetModifiedObjects() Line 323 + 0x25 bytes	C#
 	DevExpress.ExpressApp.Win.v18.2.dll!DevExpress.ExpressApp.Win.SystemModule.LockController.CheckLocking(DevExpress.ExpressApp.Win.SystemModule.LockController controller) Line 388 + 0x17 bytes	C#
 	DevExpress.ExpressApp.Win.v18.2.dll!DevExpress.ExpressApp.Win.SystemModule.LockController.CheckLocking(object obj) Line 211 + 0x9 bytes	C#
 	DevExpress.ExpressApp.Win.v18.2.dll!DevExpress.ExpressApp.Win.SystemModule.LockController.ViewObjectSpace_ObjectChanged(object sender, DevExpress.ExpressApp.ObjectChangedEventArgs e) Line 202 + 0x12 bytes	C#
 	DevExpress.ExpressApp.v18.2.dll!DevExpress.ExpressApp.BaseObjectSpace.OnObjectChanged(DevExpress.ExpressApp.ObjectChangedEventArgs args) Line 122 + 0x9 bytes	C#
 	DevExpress.ExpressApp.EF.v18.2.dll!DevExpress.ExpressApp.EF.EFObjectSpace.SetModified(object obj, DevExpress.ExpressApp.ObjectChangedEventArgs args) Line 371	C#
 	DevExpress.ExpressApp.v18.2.dll!DevExpress.ExpressApp.BaseObjectSpace.SetModified(object obj, DevExpress.ExpressApp.DC.IMemberInfo memberInfo) Line 802 + 0x34 bytes	C#
 	DevExpress.ExpressApp.v18.2.dll!DevExpress.ExpressApp.DetailView.Editor_ControlValueChanged(object sender, System.EventArgs e) Line 83 + 0x38 bytes	C#
 	DevExpress.ExpressApp.v18.2.dll!DevExpress.ExpressApp.Editors.PropertyEditor.OnControlValueChanged() Line 127 + 0x21 bytes	C#
 	DevExpress.ExpressApp.Win.v18.2.dll!DevExpress.ExpressApp.Win.Editors.DXPropertyEditor.Editor_EditValueChanged(object sender, System.EventArgs e) Line 69 + 0xa bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.Repository.RepositoryItem.RaiseEditValueChangedCore(System.EventArgs e) Line 1485 + 0x1b bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.Repository.RepositoryItem.RaiseEditValueChanged(System.EventArgs e) Line 1481 + 0xc bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.BaseEdit.RaiseEditValueChanged() Line 1270 + 0x3d bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.BaseEdit.OnEditValueChanged() Line 1259 + 0xd bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.TextEdit.OnEditValueChanged() Line 1700	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.BaseEdit.OnEditValueChanging(DevExpress.XtraEditors.Controls.ChangingEventArgs e) Line 1252	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.TextEdit.OnEditValueChanging(DevExpress.XtraEditors.Controls.ChangingEventArgs e) Line 2069	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.BaseEdit.EditValue.set(object value) Line 1196 + 0x44 bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.TextEdit.OnMaskBox_ValueChanged(object sender, System.EventArgs e) Line 1895 + 0x1e bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.Mask.MaskBox.RaiseEditTextChanged() Line 1208 + 0x2b bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.Mask.MaskBox.MaskStrategy.SimpleStrategy.DoAfterTextChanged(System.EventArgs e) Line 519 + 0x22 bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.Mask.MaskBox.OnTextChanged(System.EventArgs e) Line 1421 + 0x14 bytes	C#
 	System.Windows.Forms.dll!System.Windows.Forms.TextBoxBase.WmReflectCommand(ref System.Windows.Forms.Message m) + 0xf7 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.TextBoxBase.WndProc(ref System.Windows.Forms.Message m) + 0x42 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.TextBox.WndProc(ref System.Windows.Forms.Message m) + 0x39 bytes	
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.Mask.MaskBox.BaseWndProc(ref System.Windows.Forms.Message m) Line 285 + 0x21 bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.Mask.MaskBox.MaskStrategy.SimpleStrategy.DoWndProc(ref System.Windows.Forms.Message m) Line 528 + 0xf bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.Mask.MaskBox.WndProc(ref System.Windows.Forms.Message m) Line 1459 + 0x15 bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.TextBoxMaskBox.WndProc(ref System.Windows.Forms.Message msg) Line 1042	C#
 	System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.OnMessage(ref System.Windows.Forms.Message m) + 0x13 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.WndProc(ref System.Windows.Forms.Message m) + 0x35 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.Callback(System.IntPtr hWnd, int msg, System.IntPtr wparam, System.IntPtr lparam) + 0x80 bytes	
 	[Native to Managed Transition]	
 	[Managed to Native Transition]	
 	System.Windows.Forms.dll!System.Windows.Forms.Control.SendMessage(int msg, System.IntPtr wparam, System.IntPtr lparam) + 0x21 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Control.ReflectMessageInternal(System.IntPtr hWnd, ref System.Windows.Forms.Message m) + 0x3b bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Control.WmCommand(ref System.Windows.Forms.Message m) + 0x1b bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Control.WndProc(ref System.Windows.Forms.Message m) + 0x3b9 bytes	
 	DevExpress.Utils.v18.2.dll!DevExpress.Utils.Controls.ControlBase.WndProc(ref System.Windows.Forms.Message m) Line 185 + 0x21 bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.TextEdit.WndProc(ref System.Windows.Forms.Message msg) Line 1446 + 0x1a bytes	C#
 	System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.OnMessage(ref System.Windows.Forms.Message m) + 0x13 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.WndProc(ref System.Windows.Forms.Message m) + 0x35 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.Callback(System.IntPtr hWnd, int msg, System.IntPtr wparam, System.IntPtr lparam) + 0x80 bytes	
 	[Native to Managed Transition]	
 	[Managed to Native Transition]	
 	System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.DefWndProc(ref System.Windows.Forms.Message m) + 0x56 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Control.DefWndProc(ref System.Windows.Forms.Message m) + 0xf bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Control.WmKeyChar(ref System.Windows.Forms.Message m) + 0x2c bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Control.WndProc(ref System.Windows.Forms.Message m) + 0x88a bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.TextBoxBase.WndProc(ref System.Windows.Forms.Message m) + 0x37 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.TextBox.WndProc(ref System.Windows.Forms.Message m) + 0x39 bytes	
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.Mask.MaskBox.BaseWndProc(ref System.Windows.Forms.Message m) Line 285 + 0x21 bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.Mask.MaskBox.MaskStrategy.SimpleStrategy.DoWndProc(ref System.Windows.Forms.Message m) Line 528 + 0xf bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.Mask.MaskBox.WndProc(ref System.Windows.Forms.Message m) Line 1459 + 0x15 bytes	C#
 	DevExpress.XtraEditors.v18.2.dll!DevExpress.XtraEditors.TextBoxMaskBox.WndProc(ref System.Windows.Forms.Message msg) Line 1042	C#
 	System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.OnMessage(ref System.Windows.Forms.Message m) + 0x13 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.WndProc(ref System.Windows.Forms.Message m) + 0x35 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.Callback(System.IntPtr hWnd, int msg, System.IntPtr wparam, System.IntPtr lparam) + 0x80 bytes	
 	[Native to Managed Transition]	
 	[Managed to Native Transition]	
 	System.Windows.Forms.dll!System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(System.IntPtr dwComponentID, int reason, int pvLoopData) + 0x24d bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(int reason, System.Windows.Forms.ApplicationContext context) + 0x15f bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoop(int reason, System.Windows.Forms.ApplicationContext context) + 0x4c bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Application.Run() + 0x2e bytes	
 	DevExpress.ExpressApp.Win.v18.2.dll!DevExpress.ExpressApp.Win.WinApplication.DoApplicationRun() Line 412 + 0x5 bytes	C#
 	DevExpress.ExpressApp.Win.v18.2.dll!DevExpress.ExpressApp.Win.WinApplication.Start() Line 652	C#
>	SBD.GL.Win.exe!SBD.GL.Win.Program.Main() Line 45 + 0xa bytes	C#

Hmm something to do with self referencing

I already had the

  [InverseProperty("Parent")]

set on the Children but I experimented with putting the relationship in the OnModelCreating instead.
It made no difference.

I think about this old post
https://www.devexpress.com/Support/Center/Question/Details/T289297/how-do-i-initialise-a-child-record-with-it-s-parent-link

It turns out there is no need to do this because I am using the [Aggregated] attribute

        private void controller_ObjectCreated(object sender, ObjectCreatedEventArgs e)
        {
            var detailView = ObjectSpace.Owner as DetailView;

            if (!(detailView?.CurrentObject is Account parent)) return;
            var child = e.CreatedObject as Account;

            if (child.Parent.Id == parent.Id)
            {
                Console.WriteLine("The parent is already set");  // arrives here
            }

            child.Parent = parent;  // no need to do this

However it seems , when I experiment, that I can solve my problem by removing the aggregated tag , in which case I will need to use the controller.

#5

Ah, now I realise I forgot to upgrade to Entity Framework 6.2

#6

Still no joy

#7

OK I found a strange work around.

https://www.devexpress.com/Support/Center/Question/Details/T704563/xaf-entity-framework-self-referencing-table-example-with-aggregated-attribute-for

seems to be some kind of lazy loading bug

 var numModifiedObjects =createdObject.ObjectSpace.ModifiedObjects.Count;

Ah support had the solution

https://www.devexpress.com/Support/Center/Question/Details/T704563/xaf-entity-framework-self-referencing-table-example-with-aggregated-attribute-for

Now I can make the Accounts class work the same way and remove the HCategory2 table

#8

Part 2