XAF中FromSqlRaw构建DomainComponent嵌套ListView导航禁用问题
在我的示例项目中,我通过EntityFramework Core的FromSqlRaw方法为ListView配置数据源,核心实现代码如下:
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(); } } }
我发现该实现方式扩展性很强,但目前遇到了记录导航操作被禁用、刷新功能不生效的问题。我推测故障原因是没有底层ObjectSpace能够识别管理该ListView的集合数据源。

新增记录后,上一条、下一条导航按钮会变为可用状态,但仅支持在新增的记录间跳转。启用诊断功能后,我查到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>
该问题涉及的详情视图是通过工具栏上的自定义Action打开的,对应控制器实现代码如下:
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)); // 任意有效业务类型均可 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; } } }
请问该问题的产生原因是什么,如何修复才能让导航、刷新操作正常生效?
ThingFilterHolder被标记为[DomainComponent]非持久类,但你直接用new实例化后传入视图,同时创建的ObjectSpace是针对Thing类型的,既没有跟踪ThingFilterHolder实例本身,也没有管理其下的Things集合。ApplyFilter方法里手动新建了独立的DbContext查询数据,直接赋值给Things属性的List<Thing>完全脱离了当前视图的ObjectSpace跟踪范围。ObjectSpace拿不到集合内的对象列表、排序顺序,根本没法计算上一条/下一条的可用状态,刷新功能也因为找不到绑定的受管数据源,没法触发重新加载逻辑。- 新增记录后导航只能在新记录间跳转,是因为新建的对象会自动进入当前ObjectSpace的跟踪列表,但之前通过原生SQL查出来的老数据不在跟踪范围内,导航逻辑自然只能识别到这部分新对象。
1. 改造数据查询逻辑,让实体纳入ObjectSpace跟踪
去掉ApplyFilter方法里手动构造DbContext的逻辑,改为依赖外部传入的ObjectSpace执行查询,保证返回的实体被ObjectSpace识别跟踪:
public void ApplyFilter(IObjectSpace objectSpace) { var search = StringFunctions.SafeString(ThingFilter.Search); var efObjectSpace = (EFCoreObjectSpace)objectSpace; // 优先用XAF自带的条件构造,自动处理参数化,避免SQL注入 Things = efObjectSpace.GetObjects<Thing>( CriteriaOperator.Parse("Contains(Name, ?)", search) ).ToList(); // 必须使用原生SQL的场景参考以下写法 // var dbContext = efObjectSpace.DbContext; // Things = dbContext.Set<Thing>().FromSqlRaw( // "select Id, Name from Things where Name like {0}", // $"%{search}%" // ).ToList(); }
2. 修正ObjectSpace和视图创建逻辑
不要直接new非持久类实例,也不要用其他业务类型的ObjectSpace创建视图,改为创建对应非持久类型的ObjectSpace,通过ObjectSpace创建实例:
private void actThingScreen_Execute(object sender, SimpleActionExecuteEventArgs e) { var holderType = typeof(ThingFilterHolder); var viewId = Application.FindDetailViewId(holderType); if (SwitchToViewIfOpen(Application, viewId)) return; // 创建针对DomainComponent类型的非持久ObjectSpace var os = (NonPersistentObjectSpace)Application.CreateObjectSpace(holderType); // 通过ObjectSpace创建实例,保证实例被跟踪 var holder = os.CreateObject<ThingFilterHolder>(); // 绑定刷新事件,触发刷新时自动重新加载数据 os.Refreshing += (s, args) => { holder.ApplyFilter(os); View?.Refresh(); }; // 首次加载数据 holder.ApplyFilter(os); var detailView = Application.CreateDetailView(os, holder); // 给内嵌的Things列表视图绑定对应ObjectSpace,确保导航控制器能识别数据源 detailView.FindItem("Things")?.ControlCreated += (s, args) => { if (s is ListPropertyEditor listEditor) { listEditor.ListView.ObjectSpace = os; } }; e.ShowViewParameters.CreatedView = detailView; e.ShowViewParameters.TargetWindow = TargetWindow.NewWindow; e.ShowViewParameters.NewWindowTarget = NewWindowTarget.MdiChild; }
3. 功能验证
改造完成后,RecordsNavigationController可以通过ObjectSpace获取到完整的受跟踪实体列表,上一条/下一条导航会自动计算可用状态;点击刷新按钮时会触发ObjectSpace的Refreshing事件,重新执行查询加载最新数据,功能恢复正常。
注意:直接拼接SQL语句存在SQL注入风险,即使使用
FromSqlRaw也要通过参数化方式传入查询值,不要直接把用户输入拼到SQL字符串里。
内容的提问来源于stack exchange,提问作者Kirsten

