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

C# WinForms数据录入应用:脏数据/新增记录追踪与提交优化问询

WinForms数据录入应用:脏数据追踪与SQL提交逻辑优化咨询

当前实现方案

  • 多个业务对象对应数据录入表单,均实现INotifyPropertyChanged接口
  • 定义IRecord接口处理脏数据追踪:
public interface IRecord 
{
   bool NewRecord {get;set;}
   bool Dirty {get;set;}
   int SaveRecord();
}
  • 每个业务对象(如Region)派生实现IRecord的类型(如RegionRecord):
public class Region 
{
   public string RegionName {get;set;}
   public List<Study> Studies {get;set;}
}    

public class RegionRecord: Region, IRecord
{
   
   public bool Dirty {get;set;}
   public bool NewRecord {get;set;}

   int ID { get;set; } // ID放在此处,避免出现在基类中
   public new List<StudyRecord> Studies {get;set;}

   public int SaveRecord()
   {
       if (NewRecord)
           // 调用存储过程插入,错误时返回1
       else if (Dirty)
           // 调用存储过程更新,错误时返回1
       
       return 0;
   }
}
  • 设计初衷:分离业务对象与数据库交互逻辑,不让业务类包含ID等数据库字段
  • 表单绑定:用BindingSource关联BindingList<RegionRecord>,通过属性变更触发Dirty标记
  • 报表生成:使用OpenXML基于基类对象填充数据,但因派生类隐藏了基类Studies属性,导致报表列表数据为空,同步两个列表属性显的不合理

核心疑问

当前设计是否存在根本性错误?有没有更优的脏数据追踪、新增记录追踪及SQL提交方案?


优化方案建议

1. 用组合替代继承,解决属性隐藏问题

当前设计的核心矛盾是通过继承实现持久化逻辑,同时隐藏基类属性,这破坏了基类封装性,直接导致报表无法正常获取关联数据。建议剥离IRecord与业务对象的继承关系,改用组合模式:

// 纯净业务对象,保留所有业务属性与变更通知
public class Region : INotifyPropertyChanged
{
   private string _regionName;
   public string RegionName
   {
       get => _regionName;
       set { _regionName = value; OnPropertyChanged(); }
   }

   public ObservableCollection<Study> Studies {get;set;} = new();

   // 实现INotifyPropertyChanged接口
   public event PropertyChangedEventHandler PropertyChanged;
   protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
   {
       PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
   }
}

// 独立的持久化包装类,组合业务对象而非继承
public class RegionRecord : IRecord
{
   public Region BusinessObject { get; }
   public bool Dirty {get;set;}
   public bool NewRecord {get;set;}
   public int ID { get;set; }

   public RegionRecord(Region businessObj)
   {
       BusinessObject = businessObj;
       // 监听业务对象属性变更,自动标记Dirty
       businessObj.PropertyChanged += (s,e) => Dirty = true;
       // 监听集合变更,标记Dirty
       businessObj.Studies.CollectionChanged += (s,e) => Dirty = true;
   }

   public int SaveRecord()
   {
       if (NewRecord)
       {
           // 调用存储过程插入,使用BusinessObject的属性值
           // 插入成功后将返回的ID赋值给当前Record的ID
       }
       else if (Dirty)
       {
           // 调用存储过程更新,传入ID和BusinessObject的属性值
       }
       // 保存成功后重置Dirty状态
       Dirty = false;
       return 0;
   }
}

报表生成时直接使用RegionRecord.BusinessObject即可获取完整业务数据,完全规避属性隐藏的问题。

2. 抽象脏数据追踪逻辑,避免重复代码

将属性变更、集合变更的监听逻辑抽象到基础类中,减少每个业务Record类的重复代码:

public abstract class TrackableRecord<T> : IRecord where T : INotifyPropertyChanged
{
   public T BusinessObject { get; }
   public bool Dirty {get;set;}
   public bool NewRecord {get;set;}
   public int ID { get;set; }

   protected TrackableRecord(T businessObj)
   {
       BusinessObject = businessObj;
       businessObj.PropertyChanged += (s,e) => Dirty = true;
       // 自动监听所有ObservableCollection类型的集合变更
       HookCollectionChanges();
   }

   private void HookCollectionChanges()
   {
       foreach (var prop in typeof(T).GetProperties())
       {
           if (prop.PropertyType.IsGenericType && 
               prop.PropertyType.GetGenericTypeDefinition() == typeof(ObservableCollection<>))
           {
               var collection = prop.GetValue(BusinessObject) as INotifyCollectionChanged;
               collection?.CollectionChanged += (s,e) => Dirty = true;
           }
       }
   }

   public abstract int SaveRecord();
}

// 具体业务Record只需实现保存逻辑
public class RegionRecord : TrackableRecord<Region>
{
   public RegionRecord(Region businessObj) : base(businessObj) { }

   public override int SaveRecord()
   {
       // 插入/更新逻辑实现
       Dirty = false;
       return 0;
   }
}

3. 关联对象的脏数据处理

对于Study这类关联对象,同样创建对应的StudyRecord,在RegionRecord保存时,遍历BusinessObject.Studies,逐个处理关联StudyRecord的新增/更新逻辑。也可以在Region的Studies集合中直接存储StudyRecord,但需确保业务逻辑层仅依赖Study的属性。

4. 表单绑定调整

表单绑定仍使用BindingList<RegionRecord>,但控件绑定路径指向BusinessObject的属性,示例:

// 绑定RegionName文本框
textBoxRegionName.DataBindings.Add("Text", bindingSource, "BusinessObject.RegionName");
// DataGridView绑定Studies集合
dataGridViewStudies.DataSource = bindingSource;
dataGridViewStudies.Columns["StudyName"].DataPropertyName = "BusinessObject.Studies.StudyName";

关于业务对象是否包含ID的补充

无需刻意剥离业务对象中的ID,很多场景下ID本身就是业务对象的标识(如区域ID、用户ID)。若确实希望业务对象与数据库解耦,可以用业务唯一标识(如RegionCode)替代数据库ID做业务层的唯一判定,数据库ID仅在持久化层使用。


内容的提问来源于stack exchange,提问作者Eedz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 08:55:17