entity-framework-corexaf

Records navigation actions disabled in DomainComponent nested ListView when nested list constructed with FromSqlRaw


In my demo project I set up data for a listview via EntityFrameworkCore FromRawSql

using DevExpress.ExpressApp.DC;
using DevExpress.Persistent.Base;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Things03.Module.Functions;

namespace Things03.Module.BusinessObjects
{
    [NavigationItem("OldWayUseRibbonInstead")]
    [DomainComponent]
    public class ThingFilterHolder
    {
        public ThingFilterHolder()
        {
            ThingFilter = new ThingFilter();
            ApplyFilter();

        }
        [Browsable(false)]
        [Key]
        public int Id { get; set; }
        public ThingFilter ThingFilter { get; set; }

        public virtual List<Thing> Things { get; set; }

        public void ApplyFilter()
        {
            var search = StringFunctions.SafeString(ThingFilter.Search);
            var sql = $"select Id, Name from Things where Name like '%{search}%'";
            var db = DataFunctions.MakeDbContext();
            Things = db.Things.FromSqlRaw(sql).ToList();

        }
    }
}

I find the technique is very powerful.

However the Navigation actions are disabled and Refresh does not work.

I expect that this is because there is no underlying ObjectSpace that "knows" the listview collection.

disabled navigation buttons

The Next and Previous do enable if I add a record, but only for the added record.

If I enable diagnostics I see the following about the RecordsNavigationController

<Controller Name="RecordsNavigationController" FullName="DevExpress.ExpressApp.SystemModule.RecordsNavigationController" Active="True">
  <ActiveList>
    <Item Key="View is assigned" Value="True" />
    <Item Key="View type is ObjectView" Value="True" />
    <Item Key="PropertyEditor has ObjectSpace" Value="True" />
  </ActiveList>
  <Actions>
    <Action ID="PreviousObject" Caption="Previous Record" TypeName="SimpleAction" Category="RecordsNavigation" Active="True" Enabled="False" AdditionalInfo="">
      <ActiveList>
        <Item Key="Controller active" Value="True" />
        <Item Key="ByContext_RequireSingleObject" Value="True" />
        <Item Key="ListView or root DetailView" Value="True" />
        <Item Key="Editor doesn't support focused row selection." Value="True" />
      </ActiveList>
      <EnabledList>
        <Item Key="ByContext_RequireSingleObject" Value="True" />
        <Item Key="Can move to previous" Value="False" />
      </EnabledList>
    </Action>
    <Action ID="NextObject" Caption="Next Record" TypeName="SimpleAction" Category="RecordsNavigation" Active="True" Enabled="False" AdditionalInfo="">
      <ActiveList>
        <Item Key="Controller active" Value="True" />
        <Item Key="ByContext_RequireSingleObject" Value="True" />
        <Item Key="ListView or root DetailView" Value="True" />
        <Item Key="Editor doesn't support focused row selection." Value="True" />
      </ActiveList>
      <EnabledList>
        <Item Key="ByContext_RequireSingleObject" Value="True" />
        <Item Key="Can move to next" Value="False" />
      </EnabledList>
    </Action>
  </Actions>
</Controller>

The detail view is opened by an action on the toolbar.

using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Actions;
using DevExpress.ExpressApp.Win;
using System.Linq;
using Things03.Module.BusinessObjects;

namespace Things03.Module.Win.Controllers
{
    public class ThingFilterController : ViewController
    {
        SimpleAction actThingScreen;
        public ThingFilterController() : base()
        {
            TargetViewNesting = Nesting.Root;
            actThingScreen = new SimpleAction(this, "Things", "View");
            actThingScreen.Execute += actThingScreen_Execute;
        }
        private void actThingScreen_Execute(object sender, SimpleActionExecuteEventArgs e)
        {
            var holder = new ThingFilterHolder();
            var holderType = holder.GetType();
            var viewId = Application.FindDetailViewId(holderType);
            if (SwitchToViewIfOpen(Application, viewId)) return;

            var os = Application.CreateObjectSpace(typeof(Thing));  // any valid type would have done
            var detailView = Application.CreateDetailView(os, holder);
            e.ShowViewParameters.CreatedView = detailView;
            e.ShowViewParameters.TargetWindow = TargetWindow.NewWindow;
            e.ShowViewParameters.NewWindowTarget = NewWindowTarget.MdiChild;
        }

        private bool SwitchToViewIfOpen(XafApplication application, string viewId)
        {
            if (!(application.ShowViewStrategy is WinShowViewStrategyBase strategy)) return false;
            foreach (var win in strategy.Windows.ToArray())
            {
                if (win.View == null) continue;
                if (!win.View.Id.Equals(viewId)) continue;
                win.Show();
                return true;
            }
            return false;
        }
    }
}

Solution

  • The fix was to populate the list via ObjectSpace

    using DevExpress.ExpressApp;
    using DevExpress.ExpressApp.DC;
    using DevExpress.Persistent.Base;
    using Microsoft.EntityFrameworkCore;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using Things03.Module.Functions;
    
    namespace Things03.Module.BusinessObjects
    {
        [NavigationItem("OldWayUseRibbonInstead")]
        [DomainComponent]
        public class ThingFilterHolder 
        {
            public ThingFilterHolder()
            {
                ThingFilter = new ThingFilter();
                ApplyFilter();
    
            }
            [Browsable(false)]
            [Key]
            public int Id { get; set; }
            public ThingFilter ThingFilter { get; set; }
    
            public virtual List<Thing> Things { get; set; }
    
            [Browsable(false)] public IObjectSpace ObjectSpace { get; set; }
    
            public void ApplyFilter()
            {
                var search = StringFunctions.SafeString(ThingFilter.Search);
                var sql = $"select Id, Name from Things where Name like '%{search}%'";
                var db = DataFunctions.MakeDbContext();
                var things1 = db.Things.FromSqlRaw(sql).ToList();
                Things = new List<Thing>();
                if (ObjectSpace == null) return;
                foreach (Thing t in things1)
                {
                    Thing t2 = ObjectSpace.GetObject<Thing>(t);
                    Things.Add(t2);
                }
            }
        }
    }
    

    I was able to achieve this by adding an ObjectSpace property to the ThingFilterHolder and having the Action set the property.

    I also had to override the refresh controller.

      public class MyRefreshController : RefreshController
        {
            public MyRefreshController() : base()
            {
    
            }
    
            protected override void Refresh()
            {
    
                var holder = View.CurrentObject as ThingFilterHolder;
                holder.ApplyFilter();
                View.Refresh();
                base.Refresh();
            }
        }
    

    and the Delete

    public class MyDeleteController : DeleteObjectsViewController
    {
        public MyDeleteController() : base()
        {
            
        }
    
        protected override void Delete(SimpleActionExecuteEventArgs args)
        {
            base.Delete(args);
            View.ObjectSpace.CommitChanges();
        }
    
    }