You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 21:45:38