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
相关产品推荐
相关产品推荐

