Thin list view, fat detail view. Quick access to the right data

Entity Framework Code First combined with Dev Express XAF makes it easy to build line of business applications where the entity objects correspond to the data tables.

However for reporting reasons we sometimes want non persistent objects shaped from data pulled from multiple tables. Often times it makes sense to lazy load fields that we only need in the detail view.

In such a scenario I use a pattern that I like to call Thin List View, Fat Detail View

Note: This pattern relies on the behaviour of the DefaultListViewMasterDetailMode being set to ListViewAndDetailMode in the .xafml

image

It works as per the following example

Entity Objects

   [NavigationItem("Things")]
    [Table("Things")]
    public class Thing
    {
        [Key]
        public int Id { get; set; }
        public string ThingName { get; set; }
        public int ParentId { get; set; }
    }

 [NavigationItem("Things")]
    [Table("Details")]
    public class Detail
    {
      
         [Key]
        public int Id { get; set; }
        public string Point1 { get; set; }
        public string Point2 { get; set; }

        [ForeignKey("ThingId")]
        public virtual Thing Thing { get; set; }
        public int ThingId { get; set; }
    }

Result objects

[NavigationItem("Things")]
    [DomainComponent]
     
    public class ThinResult
    {
        public int Id { get; set; }
        public string ThingName { get; set; }

        public string ParentThingName { get; set; }

        [Browsable(false)]
        public virtual FatResult FatResult { get; set; }
        public static ThinResult[] GetList()
        {
            using (var connect = new MyDbContext())
            {
                const string sql = @"select t.Id, t.ThingName , p.ThingName as ParentThingName from things t left outer join things p on t.ParentId = p.Id ";

                return connect.Database.SqlQuery<ThinResult>(sql).ToArray();
            }
        }

        [VisibleInListView(false)]
        [ModelDefault("RowCount", "4")]
        public string Notes
        {
            get => FatResult?.Notes;
            set => FatResult.Notes = value;
        }
    }
  public class FatResult
    {
        public Thing Thing { get; set; }

        public string Notes { get; set; }
    }

Controller

  public partial class ThinResultObjectController : ObjectViewController<ListView, ThinResult>
    {
        public ThinResultObjectController()
        {
            InitializeComponent();
            TargetObjectType = typeof(ThinResult);
        }

        protected override void OnActivated()
        {
            base.OnActivated();


            var os = (NonPersistentObjectSpace) ObjectSpace;
            os.ObjectsGetting += os_ObjectsGetting;
            HandyFunctions.AddPersistentOsToNonPersistentOs(Application, os, typeof(Thing));
            View.CollectionSource.CriteriaApplied += CollectionSource_CriteriaApplied;

            View.CreateCustomCurrentObjectDetailView += View_CreateCustomCurrentObjectDetailView;
            ObjectSpace.Refresh();
        }


        private void os_ObjectsGetting(object sender, ObjectsGettingEventArgs e)
        {
            e.Objects = ThinResult.GetList().ToList();
        }


        private void View_CreateCustomCurrentObjectDetailView(object sender,
            CreateCustomCurrentObjectDetailViewEventArgs e)
        {
            if (e.ListViewCurrentObject is ThinResult currentRec)
                currentRec.FatResult = HandyFunctions.MakeFatResult(currentRec.Id, View.ObjectSpace);
        }


        protected override void OnDeactivated()
        {
            var os = (NonPersistentObjectSpace) ObjectSpace;
            os.ObjectsGetting -= os_ObjectsGetting;
            View.CollectionSource.CriteriaApplied -= CollectionSource_CriteriaApplied;
            base.OnDeactivated();

            View.CreateCustomCurrentObjectDetailView -= View_CreateCustomCurrentObjectDetailView;
            HandyFunctions.DisposeAdditionalPersistentObjectSpace(Application, os);
        }

        private void CollectionSource_CriteriaApplied(object sender, EventArgs e)
        {
            ObjectSpace.Refresh();
        }
    }

Functions

public class HandyFunctions
    {
        public static void AddPersistentOsToNonPersistentOs(XafApplication application, NonPersistentObjectSpace os, Type type)
        {
            var additionalObjectSpace = application.CreateObjectSpace(type);
            if (os.AdditionalObjectSpaces.Count == 0)
            {
                os.AdditionalObjectSpaces.Add(additionalObjectSpace);
            }
        }

        public static FatResult MakeFatResult(int thingId, IObjectSpace nonPersistentObjectSpace)
        {
            var persistentOs = GetPersistentObjectSpace(nonPersistentObjectSpace);
            var fatResult = new FatResult {Thing = new Thing { Id = thingId }};
            PopulateFatResult(fatResult, persistentOs);
            return fatResult;
        }

        private static void PopulateFatResult(FatResult fatResult, IObjectSpace os)
        {
            fatResult.Thing = os.GetObject(fatResult.Thing);
            var criteria = $"ThingId ={fatResult.Thing.Id}";
            var details = os.GetObjects<Detail>(CriteriaOperator.Parse(criteria));
            var sb = new StringBuilder();
            foreach (var detail in details)
            {
                sb.AppendLine($"[Point 1] {detail.Point1} [Point 2] {detail.Point2}");
            }

            fatResult.Notes = sb.ToString();
        }

        

        private static IObjectSpace GetPersistentObjectSpace(IObjectSpace os)
        {
            return ((NonPersistentObjectSpace)os).AdditionalObjectSpaces.FirstOrDefault();
        }

        public static void DisposeAdditionalPersistentObjectSpace(XafApplication application, NonPersistentObjectSpace os)
        {
            if (os.AdditionalObjectSpaces.Count <= 0) return;
            var additionalObjectSpace = os.AdditionalObjectSpaces.FirstOrDefault();
            os.AdditionalObjectSpaces.Remove(additionalObjectSpace);
            additionalObjectSpace?.Dispose();
        }
    }

The source code has been updated to include a list in the fat detail view.

Note, if the business object is a descendant of a base class it is important that the list is populated with objects created with the descendant, not the base class.
In order to be able to drill into the detail view AND continue to navigate with the next and previous arrows