NullReference

Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object.
< 2025年4月 >
30 31 1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 1 2 3
4 5 6 7 8 9 10

统计

有限的对象深复制,IEditableObject实现。

 

最近在项目中碰到一个这样的问题,就是在列表中双击打开编辑窗口,然后将BindingSource.Current的值传递给编辑窗体
进行数据绑定,编辑完后再刷新表格中的数据。这是一个很普通的流程,以前也经常这么做没出过什么问题。但是现在却
出现了一个麻烦。
这个麻烦就是在编辑窗口中,即使没有点击保存按钮,按取消或者直接关闭窗口回来后,列表中的数据也会更改了。仔细
想了一下,这其实是理所当然的,因为列表中绑定的实体类是引用类型的,所以传递过去的无非是指针而已,再加上在编
辑窗口中是使用DataBindings来进行数据绑定的,因此有数据更改的时候就直接更新了数据源了。原来没有发生这个情况
只不过因为原来是用代码绑定的而已。
这个问题显然不应该出现的,就算没有保存到数据库中,但是也会给用户带来困扰,所以我们应该要解决。怎么解决呢?
首先想到的就是在编辑的时候将BindingSource.Current复制一份出去编辑,这样如果用户按了保存后,再将那份Copy替
换到数据源中,如果取消当然就什么也不发生了。但是这样似乎也有问题,首先就是引用类型的复制问题,直接用另一个
变量显然是不行的,原理同上。那么就要深复制了。
单个类深复制不成问题,手写代码就可以了。但是这是实体类啊,五十多个,时间也不允许啊,何况有个类还有好几十个
字段属性的。
那么还有什么办法呢?嗯,序列化与反序列化,这样出来后的就完全是另一个崭新的对象了。基本原理可行,那么有没有
办法在不改动当前列表窗口和编辑窗口的编码的情况下完成这个功能呢?
通过查询MSDN,得知如果BindingSource的数据源项如果实现了IEditableObject的话,那么BindingSource.CancelEdit就
会自动调用该Object的CancelEdit方法,从而实现撤销编辑的功能。
可是这样一来,问题又出来了。对每一个类的反序列化只能在类的外部进行,因为在内部是没有办法将this指针赋值的。
怎么办呢?嗯哼,办法总是有的,众所周知,一个类根基的东西就是字段,类的任何外在表现其实都是与字段有关系的,
也就是说,只要我们备份了一个类的所有字段信息,那么也就是完整的备份了这个类。
明白了这个道理,一切就没有问题了。开始动手吧。
打开实体的接口定义IEntity,添加IEditableObject接口,再打开实体的基类BaseEnttiy,在这个基类里来实现该接口,
这样所有的实体类就都有了BeginEdit, CancelEdit, EndEdit等各行为了。
IEditObject接口有三个方法BeginEdit, CancelEdit, EndEdit,而BindingSource提供给外部调用的只有后二者。BeginEdit
则会由BindingSource在内部调用。

最近在项目中碰到一个这样的问题,就是在列表中双击打开编辑窗口,然后将BindingSource.Current的值传递给编辑窗体进行数据绑定,编辑完后再刷新表格中的数据。这是一个很普通的流程,以前也经常这么做没出过什么问题。但是现在却出现了一个麻烦。


这个麻烦就是在编辑窗口中,即使没有点击保存按钮,按取消或者直接关闭窗口回来后,列表中的数据也会更改了。仔细想了一下,这其实是理所当然的,因为列表中绑定的实体类是引用类型的,所以传递过去的无非是指针而已,再加上在编辑窗口中是使用DataBindings来进行数据绑定的,因此有数据更改的时候就直接更新了数据源了。原来没有发生这个情况只不过因为原来是用代码绑定的而已。


这个问题显然不应该出现的,就算没有保存到数据库中,但是也会给用户带来困扰,所以我们应该要解决。怎么解决呢?
首先想到的就是在编辑的时候将BindingSource.Current复制一份出去编辑,这样如果用户按了保存后,再将那份Copy替换到数据源中,如果取消当然就什么也不发生了。但是这样似乎也有问题,首先就是引用类型的复制问题,直接用另一个变量显然是不行的,原理同上。那么就要深复制了。


单个类深复制不成问题,手写代码就可以了。但是这是实体类啊,五十多个,时间也不允许啊,何况有个类还有好几十个字段属性的。


那么还有什么办法呢?嗯,序列化与反序列化,这样出来后的就完全是另一个崭新的对象了。基本原理可行,那么有没有办法在不改动当前列表窗口和编辑窗口的编码的情况下完成这个功能呢?


通过查询MSDN,得知如果BindingSource的数据源项如果实现了IEditableObject的话,那么BindingSource.CancelEdit就会自动调用该Object的CancelEdit方法,从而实现撤销编辑的功能。


可是这样一来,问题又出来了。对每一个类的反序列化只能在类的外部进行,因为在内部是没有办法将this指针赋值的。怎么办呢?嗯哼,办法总是有的,众所周知,一个类根基的东西就是字段,类的任何外在表现其实都是与字段有关系的,也就是说,只要我们备份了一个类的所有字段信息,那么也就是完整的备份了这个类。


明白了这个道理,一切就没有问题了。开始动手吧。


打开实体的接口定义IEntity,添加IEditableObject接口,再打开实体的基类BaseEnttiy,在这个基类里来实现该接口,这样所有的实体类就都有了BeginEdit, CancelEdit, EndEdit等各行为了。


IEditObject接口有三个方法BeginEdit, CancelEdit, EndEdit,而BindingSource提供给外部调用的只有后二者。BeginEdit则会由BindingSource在内部调用。

 

我们先来实现这个接口,想法是这样的:当调用BeginEdit的时候,我们就备份数据,调用CancelEdit的时候我们就

还原数据,调用EndEdit的时候就清空备份数据。所以至少需要两个方法:Backup 和 Restore.

 

 

#region IEditableObject 成员
 
[NonSerialized]
bool _inTx;
 
public void BeginEdit()
{
    if (!_inTx)
    {
        Backup();
        _inTx = true;
    }
}
 
public void CancelEdit()
{
    if (_inTx)
    {
        Restore();
        _inTx = false;
    }
}
 
public void EndEdit()
{
    if (_inTx)
    {
        if (____backup != null)
        {
            ____backup.Clear();
            ____backup = null;
        }
        _inTx = false;
    }
}
 
#endregion

 

 

 

值得注意的是BeginEdit会被调用多次,所以我们必须要设置一个变量来标记从而不进行多次无谓的备份。
再下来就是具体的Backup和Restore方法了,代码较长就不一一分析了,都有写注释。
#region Backup and Restore Data
 
Stack<byte[]> ____backup;
public virtual void Backup()
{
    if (____backup == null)
    {
        ____backup = new Stack<byte[]>();
        Type sourceType = this.GetType();
        HybridDictionary state = new HybridDictionary();
        FieldInfo[] fields;
 
        do
        {
            // 获取所有字段信息。
            fields = sourceType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
 
            foreach (FieldInfo field in fields)
            {
                //只处理我们自己的字段。
                if (field.DeclaringType != sourceType)
                    continue;
 
                //如果标记了NonSerialized则忽略。
                if (field.GetCustomAttributes(typeof(NonSerializedAttribute), true).Count() != 0)
                    continue;
 
                object value = field.GetValue(this);
                if (typeof(BaseEntity).IsAssignableFrom(field.FieldType))
                {
                    if (value == null)
                    {
                        //值为空,我们也要保存。
                        state.Add(field.DeclaringType.Name + "!" + field.Name, null);
                    }
                    else
                    {
                        // 不为空,而且是同一类型,则同时调用Backup方法。
                        ((BaseEntity)value).Backup();
                    }
                }
                //检查字段类型是否为ICollection,以便为集合中的项进行备份。
                else if (typeof(ICollection).IsAssignableFrom(field.FieldType) && value != null && ((ICollection)value).Count > 0)
                {
                    var col = (ICollection)value;
                    foreach (var item in col)
                    {
                        if (item is BaseEntity)
                            ((BaseEntity)item).Backup();
                    }
                     
                    state.Add(field.DeclaringType.Name + "!" + field.Name, value);
                }
                else
                {
                    //普通字段,加入字典。
                    state.Add(field.DeclaringType.Name + "!" + field.Name, value);
                }
 
            }
            //向上递归调用。
            sourceType = sourceType.BaseType;
        } while (sourceType != null);
 
        //序列化,推上栈。
        using (MemoryStream buffer = new MemoryStream())
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(buffer, state);
            ____backup.Push(buffer.ToArray());
        }
 
    }
}
 
public virtual void Restore()
{
    // 如果没有备份数据,则忽略此调用。
    if (____backup != null && ____backup.Count > 0)
    {
        //反序列化得到字典。
        HybridDictionary state;
        using (MemoryStream buffer = new MemoryStream(____backup.Pop()))
        {
            buffer.Position = 0;
            BinaryFormatter formatter = new BinaryFormatter();
            try
            {
                state = (HybridDictionary)formatter.Deserialize(buffer);
            }
            catch
            {
                return;
            }
        }
 
        object source = this;
        Type sourceType = source.GetType();
        FieldInfo[] fields;
 
        do
        {
            //获取字段信息。
            fields = sourceType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
            foreach (FieldInfo field in fields)
            {
                //只处理我们自己的字段。
                if (field.DeclaringType == sourceType)
                {
                    //获取新值。
                    object value = field.GetValue(source);
 
                    if (typeof(BaseEntity).IsAssignableFrom(field.FieldType))
                    {
                        //这是个子对象,检查只是否为空。
                        if (state.Contains(field.DeclaringType.Name + "!" + field.Name))
                        {
                            //原来为空,设置为空。
                            field.SetValue(source, null);
                        }
                        else
                        {
                            if (value != null)
                            {
                                // 子对象调用Restore方法。
                                ((BaseEntity)value).Restore();
                            }
                        }
                    }
                    //如果字典包含该字段,则还原。
                    else if (state.Contains(field.DeclaringType.Name + "!" + field.Name))
                    {
                        field.SetValue(source, state[field.DeclaringType.Name + "!" + field.Name]);
                    }
 
                    //检查值是否为集合,如果是,就对每项调用Restore方法。
                    var oldValue = state[field.DeclaringType.Name + "!" + field.Name];
                    if (oldValue != null && oldValue is ICollection)
                    {
                        var col = (ICollection)oldValue;
                        foreach (var item in col)
                        {
                            if (item is BaseEntity)
                                ((BaseEntity)item).Restore();
                        }
                    }
 
                }
            }
            //递归向上调用。
            sourceType = sourceType.BaseType;
        } while (sourceType != null);
 
        ____backup = null;
    }
}
 
#endregion
好了,这样,我们所有的实体就都有了这个功能了,这个方法不是完整的深复制而是叫做有限的深复制就是因为只有继承了BaseEntity的类的数据才会被备份和还原。不过这样在项目里已经足矣。
测试一下:
var product = ServiceFactory.GetProductService().List().Results.First();
product.Properties.Each(p => Console.WriteLine(p.PropertyID));
 
var count = product.Properties.Count();
 
product.BeginEdit();
product.Properties.Add(new ProductProperty
{
    ID = Guid.NewGuid(),
    IsNewlyAdded = true,
    PropertyID = new Guid("38b52cdb-0dbb-4183-aa0c-603d95b15885"),
    Value = "Test"
});
 
Assert.AreEqual(count + 1, product.Properties.Count());
product.CancelEdit();
Assert.AreEqual(count, product.Properties.Count());
 
var prop = product.Properties.First();
var propId = prop.PropertyID;
 
product.BeginEdit();
prop.PropertyID = Guid.NewGuid();
product.Properties.Remove(product.Properties.Last());
Assert.AreEqual(count - 1, product.Properties.Count());
product.CancelEdit();
 
Assert.AreEqual(propId, product.Properties.First().PropertyID);
Assert.AreEqual(count, product.Properties.Count());
全部通过,嗯,任务完成。:-D

posted on   NullReference  阅读(1439)  评论(1编辑  收藏  举报

编辑推荐:
· 如何在 .NET 中 使用 ANTLR4
· 后端思维之高并发处理方案
· 理解Rust引用及其生命周期标识(下)
· 从二进制到误差:逐行拆解C语言浮点运算中的4008175468544之谜
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
阅读排行:
· 想让你多爱自己一些的开源计时器
· Cursor预测程序员行业倒计时:CTO应做好50%裁员计划
· 大模型 Token 究竟是啥:图解大模型Token
· 如何在 .NET 中 使用 ANTLR4
· HttpClient使用方法总结及工具类封装
点击右上角即可分享
微信分享提示