需求描述:在工作中遇到一个比较难搞的需求,在数据上传时,需要与上一次上传的数据进行对比,并将对象属性值发生变更的属性描述出来(例如XX记录的名称由‘张三’变为‘李四’),实现一个数据对比的功能。
功能实现:在网上百度了下,参考了部分大佬的实现方式,结合自己的业务需求,最后决定使用C#反射和特性方式去实现之一功能。话不多说,直接上代码。
- 首先准备一个实体变更日志记录的类,用来保存数据跟踪对比后的结果。
public class EntityChangeLog
{
public string Operator { get; set; }
public string ClassName { get; set; }
public string DisplayName { get; set; }
public List<EntityChangeLog> Logs { get; set; }
public List<EntityChangeLogItem> Items { get; set; }
public EntityChangeLog()
{
this.Logs = new List<EntityChangeLog>();
this.Items = new List<EntityChangeLogItem>();
}
}
public class EntityChangeLogItem
{
public string DisplayName { get; set; }
public string OldValue { get; set; }
public string NewValue { get; set; }
public EntityChangeLogItem(string displayName, string oldValue, string newValue)
{
this.DisplayName = displayName;
this.OldValue = oldValue;
this.NewValue = newValue;
}
}
2.声明一个C#特性,用于标识在对比类上。
/// <summary>
/// 定义实体属性的类型
/// </summary>
public enum EnumSubItemSort
{
/// <summary>
/// 普通属性
/// </summary>
GeneralProperty,
/// <summary>
/// 子类
/// </summary>
Subclass,
/// <summary>
/// 子类集合
/// </summary>
SubclassSet
}
/// <summary>
/// 实体变更追踪特性
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class EntityChangeTrackingAttribute : Attribute
{
public string DisplayName { get; }
public Type EntityType { get; }
public EntityChangeTrackingAttribute(string displayName, Type type)
{
this.DisplayName = displayName;
this.EntityType = type;
}
}
[AttributeUsage(AttributeTargets.Property)]
public class EntityChangeTrackingItemAttribute : Attribute
{
public string DisplayName { get; }
public bool Ignore { get; }
public EnumSubItemSort SubItemSort { get; }
public Type SubItemType { get; }
public EntityChangeTrackingItemAttribute(string displayName, bool ignore = false)
{
this.DisplayName = displayName;
this.Ignore = ignore;
this.SubItemSort = EnumSubItemSort.GeneralProperty; // 调用此构造函数默认为一般属性
}
public EntityChangeTrackingItemAttribute(string displayName, EnumSubItemSort sort, Type type, bool ignore = false)
{
this.DisplayName = displayName;
this.Ignore = ignore;
this.SubItemSort = sort;
this.SubItemType = type;
}
}
3.声明一个静态类,用于扩展实体的方法。
public static class EntityChangeTrackingExtend
{
public static EntityChangeLog EntityChangeLogExtend<T>(this T oldT, T newT)
{
if (oldT == null || newT == null) throw new Exception("参与对比实体都不能为NULL!");
var echangeLog = new EntityChangeLog();
var attribute = typeof(EntityChangeTrackingAttribute);
var isDefined = Attribute.IsDefined(typeof(T), attribute); // 该类是否添加了变更特性,true标识添加
if (isDefined)
{
var calssAttr = (EntityChangeTrackingAttribute)typeof(T).GetCustomAttribute(attribute);
echangeLog.Operator = "更新";
echangeLog.DisplayName = calssAttr.DisplayName;
echangeLog.ClassName = calssAttr.EntityType.Name;
GenerateChangeLog(oldT, newT, calssAttr.EntityType, echangeLog);
}
return echangeLog;
}
private static void GenerateChangeLog(object oldT, object newT, Type entityType, EntityChangeLog echangeLog)
{
var attribute = typeof(EntityChangeTrackingItemAttribute);
var properties = entityType.GetProperties();
foreach (var prop in properties)
{
var propAttr = (EntityChangeTrackingItemAttribute)prop.GetCustomAttribute(attribute);
if (propAttr == null || propAttr.Ignore) continue; // 判断属性是否不进行跟踪记录
switch (propAttr.SubItemSort)
{
case EnumSubItemSort.GeneralProperty:
{
var oldValue = prop.GetValue(oldT) ?? null;
var newValue = prop.GetValue(newT) ?? null;
if (oldValue == null && newValue == null) continue;
if (oldValue != null && newValue != null && oldValue.Equals(newValue)) continue;
var displayName = string.IsNullOrWhiteSpace(propAttr.DisplayName) ? prop.Name : propAttr.DisplayName;
var echangeItem = new EntityChangeLogItem(displayName, oldValue?.ToString(), newValue?.ToString());
echangeLog.Items.Add(echangeItem);
break;
}
case EnumSubItemSort.Subclass:
{
var T1 = prop.GetValue(oldT) ?? null;
var T2 = prop.GetValue(newT) ?? null;
if (T1 == null && T2 == null) continue;
var log = new EntityChangeLog();
log.Operator = GetOperator(T1, T2);
log.DisplayName = propAttr.DisplayName;
log.ClassName = propAttr.SubItemType.Name;
// 注意:这里需要限定每个传递的对象必须要有一个无参数的构造函数
T1 ??= Assembly.GetExecutingAssembly().CreateInstance(propAttr.SubItemType.FullName);
T2 ??= Assembly.GetExecutingAssembly().CreateInstance(propAttr.SubItemType.FullName);
GenerateChangeLog(T1, T2, propAttr.SubItemType, log);
echangeLog.Logs.Add(log);
break;
}
case EnumSubItemSort.SubclassSet:
{
var listO = (prop.GetValue(oldT) as IEnumerable<IEntityChange>) ?? new List<IEntityChange>();
var listN = (prop.GetValue(newT) as IEnumerable<IEntityChange>) ?? new List<IEntityChange>();
if (listO.Count() == 0 && listN.Count() == 0) continue; // 子集合没有任何变更
var log = new EntityChangeLog();
log.Operator = "被更改";
log.DisplayName = propAttr.DisplayName;
log.ClassName = propAttr.SubItemType.Name;
// 1. 生成更新日志
var updateTuples = (from o in listO
from n in listN
where o.Identifies == n.Identifies
select new Tuple<IEntityChange, IEntityChange>(o, n)).ToList();
foreach (var item in updateTuples)
{
var logItem = new EntityChangeLog();
logItem.Operator = "更新";
logItem.DisplayName = $"{propAttr.DisplayName}-数据项[{item.Item1.Identifies}]";
logItem.ClassName = propAttr.SubItemType.Name;
GenerateChangeLog(item.Item1, item.Item2, propAttr.SubItemType, logItem);
log.Logs.Add(logItem);
}
// 2. 生成更新日志
var delEntities = listO.Except(listN, new EntityChangeComparer()).ToList();
foreach (var item1 in delEntities)
{
var logItem = new EntityChangeLog();
logItem.Operator = "删除";
logItem.DisplayName = $"{propAttr.DisplayName}-数据项[{item1.Identifies}]";
logItem.ClassName = propAttr.SubItemType.Name;
var item2 = Assembly.GetExecutingAssembly().CreateInstance(propAttr.SubItemType.FullName);
GenerateChangeLog(item1, item2, propAttr.SubItemType, logItem);
log.Logs.Add(logItem);
}
// 3. 生成添加日志
var addEntities = listN.Except(listO, new EntityChangeComparer()).ToList();
foreach (var item2 in addEntities)
{
var logItem = new EntityChangeLog();
logItem.Operator = "添加";
logItem.DisplayName = $"{propAttr.DisplayName}-数据项[{item2.Identifies}]";
logItem.ClassName = propAttr.SubItemType.Name;
var item1 = Assembly.GetExecutingAssembly().CreateInstance(propAttr.SubItemType.FullName);
GenerateChangeLog(item1, item2, propAttr.SubItemType, logItem);
log.Logs.Add(logItem);
}
echangeLog.Logs.Add(log);
break;
}
}
}
}
private static string GetOperator(object oldT, object newT)
{
if (oldT != null && newT == null) return "删除";
if (oldT == null && newT != null) return "添加";
return "更新";
}
}
4.处理一般的属性,例如string、int、double等类型的属性时,都比较好对比,但是遇到包含类以及包含类集合属性时处理比较麻烦,尤其是集合属性,会涉及到集合对比,所以在上面第2点我声明了一个枚举,包含普通属性、对象属性以及集合属性三个枚举值,需要分别处理。在使用使需要在特性使用的地方指定好传入对象的类型,这一点在下面会说。这里先说说我是如何处理集合属性的,首先定义了一个接口,接口中具有一个属性,用来标识两个集合对比时的数据项,也可以理解为主键,在对比时取出主键,用来判断是否增加、删除还是更新了集合中的数据。接口如下:
public interface IEntityChange
{
public string Identifies { get; }
}
public class EntityChangeComparer : IEqualityComparer<IEntityChange>
{
public bool Equals(IEntityChange x, IEntityChange y)
{
return x.Identifies == y.Identifies;
}
public int GetHashCode(IEntityChange obj)
{
return obj.Identifies.GetHashCode();
}
}
- 接下来定义我们的数据对象,把特性标识在类名和属性名上,注意子类和子类集合的用法。
/// <summary>
/// 噪声校准记录-就是我们需要对比的数据主表
/// </summary>
[EntityChangeTracking("噪声校准记录", typeof(NoiseCalibration))]
public class NoiseCalibration
{
[EntityChangeTrackingItem("项目ID")]
public Guid ProjectId { get; set; }
[EntityChangeTrackingItem("采样日期")]
public DateTime SamplingDate { get; set; }
[EntityChangeTrackingItem("测试设备")]
public string TestInstrument { get; set; }
[EntityChangeTrackingItem("日间数据", EnumSubItemSort.Subclass, typeof(NoiseCalibrationItem))]
public NoiseCalibrationItem Daytime { get; set; }
[EntityChangeTrackingItem("夜间数据", EnumSubItemSort.Subclass, typeof(NoiseCalibrationItem))]
public NoiseCalibrationItem Nighttime { get; set; }
[EntityChangeTrackingItem("采样照片", EnumSubItemSort.SubclassSet, typeof(PhotoInfo))]
public List<PhotoInfo> PhotoList { get; set; }
[EntityChangeTrackingItem("采样记录", EnumSubItemSort.SubclassSet, typeof(NoiseRecord))]
public List<NoiseRecord> Records { get; set; }
}
/// <summary>
/// 包含类对象
/// </summary>
public class NoiseCalibrationItem
{
[EntityChangeTrackingItem("采样前时间")]
public DateTime? BeforeTestTime { get; set; }
[EntityChangeTrackingItem("采样前数据")]
public string BeforeTestValue { get; set; }
[EntityChangeTrackingItem("采样前结果")]
public bool BeforeValueIsNormal { get; set; }
[EntityChangeTrackingItem("采样后时间")]
public DateTime? AfterTestTime { get; set; }
[EntityChangeTrackingItem("采样后数据")]
public string AfterTestValue { get; set; }
[EntityChangeTrackingItem("采样前结果")]
public bool AfterValueIsNormal { get; set; }
[EntityChangeTrackingItem("校准值")]
public double? CalibrationValue { get; set; }
}
/// <summary>
/// 包含类对象-集合属性用,这里需要继承IEntityChange,将主键字段进行绑定
/// </summary>
public class PhotoInfo : IEntityChange
{
[EntityChangeTrackingItem("照片名称")]
public string Name { get; set; }
[EntityChangeTrackingItem("照片描述")]
public string Desc { get; set; }
[EntityChangeTrackingItem("上传人员")]
public string UploadUser { get; set; }
public string Date { get; set; }
public string Identifies => this.Date;
}
/// <summary>
/// 包含类对象-集合属性,集合中嵌套集合,这里需要继承IEntityChange,将主键字段进行绑定
/// </summary>
public class NoiseRecord : IEntityChange
{
public string Date { get; set; }
[EntityChangeTrackingItem("开始时间")]
public DateTime Start { get; set; }
[EntityChangeTrackingItem("结束时间")]
public DateTime End { get; set; }
[EntityChangeTrackingItem("时长")]
public int Length { get; set; }
[EntityChangeTrackingItem("参考值")]
public float Value { get; set; }
public string Identifies => this.Date;
[EntityChangeTrackingItem("采样明细", EnumSubItemSort.SubclassSet, typeof(NoiseRecordItem))]
public List<NoiseRecordItem> RecordItems { get; set; }
}
/// <summary>
/// 包含类对象-集合属性-嵌套的子集合对象,这里需要继承IEntityChange,将主键字段进行绑定
/// </summary>
public class NoiseRecordItem : IEntityChange
{
public string Parameter { get; set; }
[EntityChangeTrackingItem("标准值")]
public double StandardValue { get; set; }
[EntityChangeTrackingItem("测试值")]
public double TestValue { get; set; }
[EntityChangeTrackingItem("测试结果")]
public bool TestResult { get; set; }
[EntityChangeTrackingItem("测试备注")]
public string TestRemark { get; set; }
public string Identifies => this.Parameter;
}
- 模拟创建实体数据,调用扩展方法,生成跟踪记录。
public static void Execute()
{
var tmodel1 = new NoiseCalibration()
{
ProjectId = new Guid(),
SamplingDate = new DateTime(2021, 10, 1, 10, 8, 9),
TestInstrument = "TAX09832",
//Daytime = new NoiseCalibrationItem()
//{
// AfterTestTime = new DateTime(2021, 10, 1),
// AfterTestValue = "500.00",
// AfterValueIsNormal = true,
// BeforeTestTime = new DateTime(2021, 10, 2),
// BeforeTestValue = "501.00",
// BeforeValueIsNormal = false,
// CalibrationValue = 785.11
//},
Nighttime = new NoiseCalibrationItem()
{
AfterTestTime = new DateTime(2021, 10, 1),
AfterTestValue = "600.00",
AfterValueIsNormal = true,
BeforeTestTime = new DateTime(2021, 10, 2),
BeforeTestValue = "601.00",
BeforeValueIsNormal = false,
CalibrationValue = 456.5
},
PhotoList = new List<PhotoInfo>()
{
new PhotoInfo() { Date = "2021-11-18", Name = "0.jpg", Desc = "小朋友", UploadUser = "朱上" },
new PhotoInfo() { Date = "2021-11-19", Name = "1.jpg", Desc = "风景美如画", UploadUser = "秦岚" }
},
Records = new List<NoiseRecord>()
{
new NoiseRecord()
{
Date = "2021-11-19",
Start = new DateTime(2021,11,19,07,05,06),
End = new DateTime(2021,11,19,08,05,06),
Length = 60,
Value = 95.5f,
RecordItems = new List<NoiseRecordItem>(){
new NoiseRecordItem(){ Parameter = "二氧化碳", StandardValue = 9.9, TestValue = 8.9, TestResult = true, TestRemark = "棒棒的1"},
new NoiseRecordItem(){ Parameter = "一氧化碳", StandardValue = 8.9, TestValue = 8.9, TestResult = true, TestRemark = "棒棒的2"}
}
},
new NoiseRecord()
{
Date = "2021-11-20",
Start = new DateTime(2021,11,20,07,05,06),
End = new DateTime(2021,11,20,08,05,06),
Length = 60,
Value = 85.6f,
RecordItems = new List<NoiseRecordItem>(){
new NoiseRecordItem(){ Parameter = "二氧化碳", StandardValue = 9.9, TestValue = 8.9, TestResult = true, TestRemark = "棒棒的3"},
new NoiseRecordItem(){ Parameter = "一氧化碳", StandardValue = 8.9, TestValue = 8.9, TestResult = true, TestRemark = "棒棒的4"}
}
}
}
};
var tmodel2 = new NoiseCalibration()
{
ProjectId = new Guid(),
SamplingDate = new DateTime(2021, 10, 2, 10, 8, 9),
TestInstrument = "TAX034333",
Daytime = new NoiseCalibrationItem()
{
AfterTestTime = new DateTime(2021, 10, 2),
AfterTestValue = "499.00",
AfterValueIsNormal = true,
BeforeTestTime = new DateTime(2021, 10, 3),
BeforeTestValue = "502.00",
BeforeValueIsNormal = false,
CalibrationValue = 500.00
},
Nighttime = new NoiseCalibrationItem()
{
AfterTestTime = new DateTime(2021, 10, 2),
AfterTestValue = "595.00",
AfterValueIsNormal = true,
BeforeTestTime = new DateTime(2021, 10, 3),
BeforeTestValue = "603.00",
BeforeValueIsNormal = false,
CalibrationValue = 600.00
},
PhotoList = new List<PhotoInfo>()
{
new PhotoInfo(){ Date = "2021-11-19", Name = "2.jpg", Desc = "桂林山水甲天下", UploadUser = "李云龙" },
new PhotoInfo(){ Date = "2021-11-20", Name = "4.jpg", Desc = "天下无双好风光", UploadUser = "猪八戒"}
},
Records = new List<NoiseRecord>()
{
new NoiseRecord()
{
Date = "2021-11-19",
Start = new DateTime(2021,11,19,10,05,06),
End = new DateTime(2021,11,19,11,00,06),
Length = 55,
Value = 95.6f,
RecordItems = new List<NoiseRecordItem>(){
new NoiseRecordItem(){ Parameter = "二氧化碳", StandardValue = 85.5, TestValue = 88.9, TestResult = false, TestRemark = "差差的1"},
new NoiseRecordItem(){ Parameter = "一氧化碳", StandardValue = 88.9, TestValue = 88.9, TestResult = false, TestRemark = "差差的2"}
}
},
new NoiseRecord()
{
Date = "2021-11-20",
Start = new DateTime(2021,11,20,11,05,06),
End = new DateTime(2021,11,20,12,00,06),
Length = 55,
Value = 185.6f,
RecordItems = new List<NoiseRecordItem>(){
new NoiseRecordItem(){ Parameter = "二氧化碳", StandardValue = 19.9, TestValue = 18.9, TestResult = false, TestRemark = "差差的3"},
new NoiseRecordItem(){ Parameter = "一氧化碳", StandardValue = 18.9, TestValue = 18.9, TestResult = false, TestRemark = "差差的4"}
}
},
new NoiseRecord()
{
Date = "2021-11-21",
Start = new DateTime(2021,11,21,11,05,06),
End = new DateTime(2021,11,21,12,00,06),
Length = 55,
Value = 185.6f,
RecordItems = new List<NoiseRecordItem>(){
new NoiseRecordItem(){ Parameter = "二氧化碳", StandardValue = 119.9, TestValue = 118.9, TestResult = false, TestRemark = "差差的5"},
new NoiseRecordItem(){ Parameter = "一氧化碳", StandardValue = 118.9, TestValue = 118.9, TestResult = false, TestRemark = "差差的6"}
}
}
}
};
var log = tmodel1.EntityChangeLogExtend(tmodel2);
// 使用Newtonsoft.Json将生成的记录转成JSON字符串输出到屏幕
Console.WriteLine(JsonConvert.SerializeObject(log));
}
输出结果:
看着有点乱,可以格式化下:
{
"Operator":"更新",
"ClassName":"NoiseCalibration",
"DisplayName":"噪声校准记录",
"Logs":[
{
"Operator":"添加",
"ClassName":"NoiseCalibrationItem",
"DisplayName":"日间数据",
"Logs":[
],
"Items":[
{
"DisplayName":"采样前时间",
"OldValue":null,
"NewValue":"2021/10/3 0:00:00"
},
{
"DisplayName":"采样前数据",
"OldValue":null,
"NewValue":"502.00"
},
{
"DisplayName":"采样后时间",
"OldValue":null,
"NewValue":"2021/10/2 0:00:00"
},
{
"DisplayName":"采样后数据",
"OldValue":null,
"NewValue":"499.00"
},
{
"DisplayName":"采样前结果",
"OldValue":"False",
"NewValue":"True"
},
{
"DisplayName":"校准值",
"OldValue":null,
"NewValue":"500"
}
]
},
{
"Operator":"更新",
"ClassName":"NoiseCalibrationItem",
"DisplayName":"夜间数据",
"Logs":[
],
"Items":[
{
"DisplayName":"采样前时间",
"OldValue":"2021/10/2 0:00:00",
"NewValue":"2021/10/3 0:00:00"
},
{
"DisplayName":"采样前数据",
"OldValue":"601.00",
"NewValue":"603.00"
},
{
"DisplayName":"采样后时间",
"OldValue":"2021/10/1 0:00:00",
"NewValue":"2021/10/2 0:00:00"
},
{
"DisplayName":"采样后数据",
"OldValue":"600.00",
"NewValue":"595.00"
},
{
"DisplayName":"校准值",
"OldValue":"456.5",
"NewValue":"600"
}
]
},
{
"Operator":"被更改",
"ClassName":"PhotoInfo",
"DisplayName":"采样照片",
"Logs":[
{
"Operator":"更新",
"ClassName":"PhotoInfo",
"DisplayName":"采样照片-数据项[2021-11-19]",
"Logs":[
],
"Items":[
{
"DisplayName":"照片名 称",
"OldValue":"1.jpg",
"NewValue":"2.jpg"
},
{
"DisplayName":"照片描述",
"OldValue":"风景美如画",
"NewValue":"桂林山水甲天下"
},
{
"DisplayName":"上传人员",
"OldValue":"秦岚",
"NewValue":"李云 龙"
}
]
},
{
"Operator":"删除",
"ClassName":"PhotoInfo",
"DisplayName":"采样照片-数据项[2021-11-18]",
"Logs":[
],
"Items":[
{
"DisplayName":"照片名称",
"OldValue":"0.jpg",
"NewValue":null
},
{
"DisplayName":"照片描述",
"OldValue":"小朋友",
"NewValue":null
},
{
"DisplayName":"上传人员",
"OldValue":"朱上",
"NewValue":null
}
]
},
{
"Operator":"添加",
"ClassName":"PhotoInfo",
"DisplayName":"采样照 片-数据项[2021-11-20]",
"Logs":[
],
"Items":[
{
"DisplayName":"照片名称",
"OldValue":null,
"NewValue":"4.jpg"
},
{
"DisplayName":"照片描述",
"OldValue":null,
"NewValue":"天下无双好风光"
},
{
"DisplayName":"上传人员",
"OldValue":null,
"NewValue":"猪八戒"
}
]
}
],
"Items":[
]
},
{
"Operator":"被更改",
"ClassName":"NoiseRecord",
"DisplayName":"采样记录",
"Logs":[
{
"Operator":"更新",
"ClassName":"NoiseRecord",
"DisplayName":"采样记录-数据项[2021-11-19]",
"Logs":[
{
"Operator":"被更改",
"ClassName":"NoiseRecordItem",
"DisplayName":"采样明细",
"Logs":[
{
"Operator":"更新",
"ClassName":"NoiseRecordItem",
"DisplayName":"采样明细-数据项[二氧化碳]",
"Logs":[
],
"Items":[
{
"DisplayName":"标准值",
"OldValue":"9.9",
"NewValue":"85.5"
},
{
"DisplayName":"测试值",
"OldValue":"8.9",
"NewValue":"88.9"
},
{
"DisplayName":"测试结果",
"OldValue":"True",
"NewValue":"False"
},
{
"DisplayName":"测试备注",
"OldValue":"棒棒的1",
"NewValue":"差差的1"
}
]
},
{
"Operator":"更新",
"ClassName":"NoiseRecordItem",
"DisplayName":"采样明细-数据项[一氧化碳]",
"Logs":[
],
"Items":[
{
"DisplayName":"标准值",
"OldValue":"8.9",
"NewValue":"88.9"
},
{
"DisplayName":"测试值",
"OldValue":"8.9",
"NewValue":"88.9"
},
{
"DisplayName":"测试结果",
"OldValue":"True",
"NewValue":"False"
},
{
"DisplayName":"测试备注",
"OldValue":"棒棒的2",
"NewValue":"差差的2"
}
]
}
],
"Items":[
]
}
],
"Items":[
{
"DisplayName":"开始时间",
"OldValue":"2021/11/19 7:05:06",
"NewValue":"2021/11/19 10:05:06"
},
{
"DisplayName":"结束时间",
"OldValue":"2021/11/19 8:05:06",
"NewValue":"2021/11/19 11:00:06"
},
{
"DisplayName":"时长",
"OldValue":"60",
"NewValue":"55"
},
{
"DisplayName":"参考值",
"OldValue":"95.5",
"NewValue":"95.6"
}
]
},
{
"Operator":"更新",
"ClassName":"NoiseRecord",
"DisplayName":"采样记录-数据项[2021-11-20]",
"Logs":[
{
"Operator":"被更改",
"ClassName":"NoiseRecordItem",
"DisplayName":"采样明细",
"Logs":[
{
"Operator":"更新",
"ClassName":"NoiseRecordItem",
"DisplayName":"采样明细-数据项[二氧化碳]",
"Logs":[
],
"Items":[
{
"DisplayName":"标准值",
"OldValue":"9.9",
"NewValue":"19.9"
},
{
"DisplayName":"测试值",
"OldValue":"8.9",
"NewValue":"18.9"
},
{
"DisplayName":"测试结果",
"OldValue":"True",
"NewValue":"False"
},
{
"DisplayName":"测试备注",
"OldValue":"棒棒的3",
"NewValue":"差差的3"
}
]
},
{
"Operator":"更新",
"ClassName":"NoiseRecordItem",
"DisplayName":"采样明细-数据项[ 一氧化碳]",
"Logs":[
],
"Items":[
{
"DisplayName":"标准值",
"OldValue":"8.9",
"NewValue":"18.9"
},
{
"DisplayName":"测试值",
"OldValue":"8.9",
"NewValue":"18.9"
},
{
"DisplayName":"测试结果",
"OldValue":"True",
"NewValue":"False"
},
{
"DisplayName":"测试备注",
"OldValue":"棒棒的4",
"NewValue":"差差的4"
}
]
}
],
"Items":[
]
}
],
"Items":[
{
"DisplayName":"开始时间",
"OldValue":"2021/11/20 7:05:06",
"NewValue":"2021/11/20 11:05:06"
},
{
"DisplayName":"结束时间",
"OldValue":"2021/11/20 8:05:06",
"NewValue":"2021/11/20 12:00:06"
},
{
"DisplayName":"时长",
"OldValue":"60",
"NewValue":"55"
},
{
"DisplayName":"参考值",
"OldValue":"85.6",
"NewValue":"185.6"
}
]
},
{
"Operator":"添加",
"ClassName":"NoiseRecord",
"DisplayName":"采样记录-数据项[2021-11-21]",
"Logs":[
{
"Operator":"被更改",
"ClassName":"NoiseRecordItem",
"DisplayName":"采样明细",
"Logs":[
{
"Operator":"添加",
"ClassName":"NoiseRecordItem",
"DisplayName":"采样明细-数据项[二氧化碳]",
"Logs":[
],
"Items":[
{
"DisplayName":"标准值",
"OldValue":"0",
"NewValue":"119.9"
},
{
"DisplayName":"测试值",
"OldValue":"0",
"NewValue":"118.9"
},
{
"DisplayName":"测试备注",
"OldValue":null,
"NewValue":"差差的5"
}
]
},
{
"Operator":"添加",
"ClassName":"NoiseRecordItem",
"DisplayName":"采样明细-数据项[一氧化碳]",
"Logs":[
],
"Items":[
{
"DisplayName":"标准值",
"OldValue":"0",
"NewValue":"118.9"
},
{
"DisplayName":"测试值",
"OldValue":"0",
"NewValue":"118.9"
},
{
"DisplayName":"测试备注",
"OldValue":null,
"NewValue":"差差的6"
}
]
}
],
"Items":[
]
}
],
"Items":[
{
"DisplayName":"开始时间",
"OldValue":"0001/1/1 0:00:00",
"NewValue":"2021/11/21 11:05:06"
},
{
"DisplayName":"结束时间",
"OldValue":"0001/1/1 0:00:00",
"NewValue":"2021/11/21 12:00:06"
},
{
"DisplayName":"时长",
"OldValue":"0",
"NewValue":"55"
},
{
"DisplayName":"参考值",
"OldValue":"0",
"NewValue":"185.6"
}
]
}
],
"Items":[
]
}
],
"Items":[
{
"DisplayName":"采样日期",
"OldValue":"2021/10/1 10:08:09",
"NewValue":"2021/10/2 10:08:09"
},
{
"DisplayName":"测试设备",
"OldValue":"TAX09832",
"NewValue":"TAX034333"
}
]
}
- 至此就可以拿到我们的变更记录了。
8.将变更记录转成PDF格式的文件,是另一个需求,使用Itext7操作。
using System;
using System.IO;
using iText.Kernel.Events;
using iText.Kernel.Geom;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;
using iText.Layout;
using iText.Layout.Element;
using iText.Kernel.Colors;
using iText.Layout.Properties;
using iText.Layout.Borders;
using iText.Kernel.Font;
using iText.IO.Font;
using static iText.Kernel.Font.PdfFontFactory;
using iText.IO.Image;
using Favor.Example.RecordDiff;
using System.Collections.Generic;
namespace Favor.Example.Itext
{
public class PdfHelper
{
public static void GenerateEntityChangePdf(EntityChangeLog changeLog)
{
var filePath = $@"...\DateTime.Now.ToFileTime()}.pdf"; // 随便声明路径
var fontPath = $@"...\SIMHEI_0.TTF";// 声明字体路径
var imageLogo = $@"...\SGS_RGB_12mm.bmp"; // 声明页眉图片
using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
using (var pdfdocument = new PdfDocument(new PdfWriter(fileStream)))
{
var pdfFont = PdfFontFactory.CreateFont(fontPath, PdfEncodings.IDENTITY_H, EmbeddingStrategy.PREFER_EMBEDDED);
var document = new Document(pdfdocument, PageSize.A4).SetFont(pdfFont);
var docwidth = pdfdocument.GetDefaultPageSize().GetWidth();
var tableWidth = docwidth - document.GetRightMargin() - document.GetLeftMargin();
pdfdocument.AddEventHandler(PdfDocumentEvent.END_PAGE, new HeaderFooterHandler(imageLogo, pdfFont));
document.Add(CreatePara($"{changeLog.DisplayName}-{changeLog.Operator}明细"));
var table = new Table(new float[3]).UseAllAvailableWidth();
GenEntityLogItem(table, changeLog.Items, changeLog.Operator);
GenEntityLog(table, changeLog.Logs);
document.Add(table);
document.Close();
}
}
}
private static void GenEntityLogItem(Table table, List<EntityChangeLogItem> logItems, string oper)
{
if (logItems.Count == 0) return;
AddTCell(table, "字段", 100);
AddTCell(table, "旧值", null);
AddTCell(table, "新值", null);
foreach (var item in logItems)
{
AddTCell(table, item.DisplayName, 100);
AddTCell(table, CellOldValue(item.OldValue, oper), TextAlignment.LEFT);
AddTCell(table, CellNewValue(item.NewValue, oper), TextAlignment.LEFT);
}
}
private static string CellOldValue(string value,string oper)
{
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
if (oper == "添加") return string.Empty;
return value;
}
private static string CellNewValue(string value, string oper)
{
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
if (oper == "删除") return string.Empty;
return value;
}
public static void AddTCell(Table table, string content, float? width)
{
var cell = new Cell();
if (width.HasValue)
{
cell.SetWidth(width.Value);
}
cell.SetBold();
cell.SetBackgroundColor(ColorConstants.LIGHT_GRAY, 0.5f);
cell.Add(CreatePara(content, 10));
table.AddCell(cell);
}
public static void AddTCell(Table table, string content, TextAlignment alignment)
{
var cell = new Cell().SetMinWidth(200).Add(CreatePara(content, 10, alignment));
table.AddCell(cell);
}
private static void GenEntityLog(Table table, List<EntityChangeLog> logs)
{
if (logs.Count == 0) return;
foreach (var log in logs)
{
if (log.Operator == "被更改")
{
var cell0 = new Cell(1,3);
cell0.SetHeight(5).SetBackgroundColor(ColorConstants.BLACK, 0.5f);
table.AddCell(cell0);
}
var cell1 = new Cell(1,3);
cell1.Add(CreatePara($"{log.DisplayName}-{log.Operator}"));
table.AddCell(cell1);
GenEntityLogItem(table, log.Items, log.Operator);
GenEntityLog(table, log.Logs);
}
}
private static Paragraph CreatePara(string text,float fontSize = 12f, TextAlignment alignment = TextAlignment.CENTER)
{
return new Paragraph(text).SetMargin(1f)
.SetFontSize(fontSize)
.SetTextAlignment(alignment);
}
public class HeaderFooterHandler : IEventHandler
{
private readonly PdfFont Font;
private readonly string ImageLogo;
public HeaderFooterHandler(string imageLogo,PdfFont font)
{
this.ImageLogo = imageLogo;
this.Font = font;
}
public void HandleEvent(Event @event)
{
try
{
PdfDocumentEvent docEvent = (PdfDocumentEvent)@event;
PdfDocument pdfDoc = docEvent.GetDocument();
Document doc = new Document(pdfDoc);
PdfPage page = docEvent.GetPage();
int pageNumber = pdfDoc.GetPageNumber(page);
Rectangle pageSize = page.GetPageSize();
var pdfWidth = pageSize.GetWidth();
var pdfHeight = pageSize.GetHeight();
PdfCanvas pdfCanvas = new PdfCanvas(page.NewContentStreamBefore(), page.GetResources(), pdfDoc);
Color lineColor = new DeviceRgb(133, 135, 134);
pdfCanvas.SetLineWidth(1.5f).SetStrokeColor(lineColor);
var tableWidth = pdfWidth - doc.GetRightMargin() - doc.GetLeftMargin();
// 1.生成页眉
var x0 = doc.GetRightMargin();
var y0 = pdfHeight - doc.GetTopMargin();
pdfCanvas.MoveTo(x0, y0).LineTo(pdfWidth - doc.GetRightMargin(), y0).Stroke();
var headerTable = new Table(2)
.SetHeight(30).SetFixedLayout()
.SetWidth(tableWidth).SetHorizontalAlignment(HorizontalAlignment.CENTER);
var image = new Image(ImageDataFactory.Create(this.ImageLogo));
var cellhead1 = new Cell().SetBorder(Border.NO_BORDER).Add(image)
.SetVerticalAlignment(VerticalAlignment.BOTTOM);
headerTable.AddCell(cellhead1);
var paraHead2 = new Paragraph()
.SetVerticalAlignment(VerticalAlignment.BOTTOM)
.Add(new Tab()).AddTabStops(new TabStop(1000, TabAlignment.RIGHT))
.Add("页眉内容").SetFontSize(10f).SetFontColor(lineColor);
var cellHead2 = new Cell().SetBorder(Border.NO_BORDER).Add(paraHead2).SetFont(this.Font);
headerTable.AddCell(cellHead2);
// 设置页眉位置
headerTable.SetFixedPosition(doc.GetLeftMargin(), pdfHeight - doc.GetTopMargin(), tableWidth);
doc.Add(headerTable);
// 2.生成页脚
pdfCanvas.MoveTo(x0, doc.GetBottomMargin()).LineTo(pdfWidth - doc.GetRightMargin(), doc.GetBottomMargin()).Stroke();
var footerTable = new Table(2)
.SetHeight(40).SetFixedLayout()
.SetWidth(tableWidth).SetHorizontalAlignment(HorizontalAlignment.CENTER);
var paraFoot1 = new Paragraph()
.SetVerticalAlignment(VerticalAlignment.BOTTOM)
.Add($"第{pageNumber}页/共{pdfDoc.GetNumberOfPages()}页")
.SetFontSize(8f).SetFontColor(lineColor);
var cellFoot1 = new Cell().SetBorder(Border.NO_BORDER).Add(paraFoot1).SetFont(this.Font);
footerTable.AddCell(cellFoot1);
var paraFoot2 = new Paragraph()
.SetVerticalAlignment(VerticalAlignment.BOTTOM)
.Add(new Tab()).AddTabStops(new TabStop(1000, TabAlignment.RIGHT))
.Add($"生成日期:{DateTime.Now.ToLongDateString()}")
.SetFontSize(8f).SetFontColor(lineColor);
var cellFoot2 = new Cell().SetBorder(Border.NO_BORDER).Add(paraFoot2).SetFont(this.Font);
footerTable.AddCell(cellFoot2);
// 设置页脚位置
footerTable.SetFixedPosition(doc.GetLeftMargin(), doc.GetBottomMargin() - footerTable.GetHeight().GetValue(), tableWidth);
doc.Add(footerTable);
}
catch (Exception ex)
{
throw;
}
}
}
}
-
调用GenerateEntityChangePdf()方法,生成PDF文件。如下:
基本上实现了想要的需求,欢迎各位大佬提意见哈!!!