我正在尝试为 Windows 窗体应用程序实现加载/保存功能。
我有以下组件:
- 树视图
- 几个列表视图
- 几个文本框
- 几个对象(其中包含一个大字典列表)
我想实现一种将所有这些保存到文件中的方法,并在以后恢复/加载它。
最好的方法是什么?
我认为 XML 序列化是要走的路,但我不太确定如何或从哪里开始。还是需要一个非常复杂的解决方案才能做到这一点?
理想情况下,您不应该保留 UI 状态;你应该坚持代表你的数据的一些对象模型的状态。除了 之外TreeView
,使用数据绑定将对象模型绑定到 UI 是相当简单的。这可以是DataTable
基于 - 的方法,也可以是自定义类层次结构(我的偏好)。
将数据与 UI 分离后,保存数据就很简单了。诸如此类的例子很多XmlSerializer
。
这是一个将对象和一些祖先绑定到 UI 的示例;在这里使用 C# 3.0 纯粹是为了简洁 - 一切都适用于 C# 2.0。
这里的大部分代码是设置表单和/或处理属性更改通知 - 重要的是,没有任何代码专门用于从对象模型更新 UI,或从 UI 更新对象模型。
另请注意,IDE 可以为您执行大量数据绑定代码,只需将 BindingSource 拖放到表单上并通过属性网格中的对话框将 DataSource 设置为类型。
请注意,提供属性更改通知(PropertyChanged 的东西)不是必需的 - 但是,如果您确实实现了这一点,大多数 2-way UI 绑定将工作得更好。并不是说 PostSharp 有一些有趣的方法可以用最少的代码做到这一点。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Windows.Forms;
using System.Xml.Serialization;
static class Program { // formatted for vertical space
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Button load, save, newCust;
BindingSource source = new BindingSource { DataSource = typeof(Customer) };
XmlSerializer serializer = new XmlSerializer(typeof(Customer));
using (Form form = new Form {
DataBindings = {{"Text", source, "Name"}}, // show customer name as form title
Controls = {
new DataGridView { Dock = DockStyle.Fill, // grid of orders
DataSource = source, DataMember = "Orders"},
new TextBox { Dock = DockStyle.Top, ReadOnly = true, // readonly order ref
DataBindings = {{"Text", source, "Orders.OrderRef"}}},
new TextBox { Dock = DockStyle.Top, // editable customer name
DataBindings = {{"Text", source, "Name"}}},
(save = new Button { Dock = DockStyle.Bottom, Text = "save" }),
(load = new Button{ Dock = DockStyle.Bottom, Text = "load"}),
(newCust = new Button{ Dock = DockStyle.Bottom, Text = "new"}),
}
})
{
const string PATH = "customer.xml";
form.Load += delegate {
newCust.PerformClick(); // create new cust when loading form
load.Enabled = File.Exists(PATH);
};
save.Click += delegate {
using (var stream = File.Create(PATH)) {
serializer.Serialize(stream, source.DataSource);
}
load.Enabled = true;
};
load.Click += delegate {
using (var stream = File.OpenRead(PATH)) {
source.DataSource = serializer.Deserialize(stream);
}
};
newCust.Click += delegate {
source.DataSource = new Customer();
};
Application.Run(form);
}
}
}
[Serializable]
public sealed class Customer : NotifyBase {
private int customerId;
[DisplayName("Customer Number")]
public int CustomerId {
get { return customerId; }
set { SetField(ref customerId, value, "CustomerId"); }
}
private string name;
public string Name {
get { return name; }
set { SetField(ref name, value, "Name"); }
}
public List<Order> Orders { get; set; } // XmlSerializer demands setter
public Customer() {
Orders = new List<Order>();
}
}
[Serializable]
public sealed class Order : NotifyBase {
private int orderId;
[DisplayName("Order Number")]
public int OrderId {
get { return orderId; }
set { SetField(ref orderId, value, "OrderId"); }
}
private string orderRef;
[DisplayName("Reference")]
public string OrderRef {
get { return orderRef; }
set { SetField(ref orderRef, value, "OrderRef"); }
}
private decimal orderValue, carriageValue;
[DisplayName("Order Value")]
public decimal OrderValue {
get { return orderValue; }
set {
if (SetField(ref orderValue, value, "OrderValue")) {
OnPropertyChanged("TotalValue");
}
}
}
[DisplayName("Carriage Value")]
public decimal CarriageValue {
get { return carriageValue; }
set {
if (SetField(ref carriageValue, value, "CarriageValue")) {
OnPropertyChanged("TotalValue");
}
}
}
[DisplayName("Total Value")]
public decimal TotalValue { get { return OrderValue + CarriageValue; } }
}
[Serializable]
public class NotifyBase { // purely for convenience
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetField<T>(ref T field, T value, string propertyName) {
if (!EqualityComparer<T>.Default.Equals(field, value)) {
field = value;
OnPropertyChanged(propertyName);
return true;
}
return false;
}
protected virtual void OnPropertyChanged(string propertyName) {
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
是的,您绝对应该为此使用 XML 序列化。但正如 Marc Gravell 所指出的,您必须首先拥有保存 GUI 组件显示的数据的对象。然后,您实际上可以用最少的代码行自动(反)序列化。
上面的示例存在问题。考虑到最终您的应用程序已更新。您的对象模型可能会发生巨大变化,因此无法反序列化。您可以做一些事情来确保从 xml 版本 1 的反序列化可以反序列化为版本 2 中的对象模型,但是如果您有可能进行重大的结构更改,则 xml 反序列化不是要走的路。
如果是这种情况并且您的应用程序已部署给客户,我强烈建议您进一步查看您的保存/加载逻辑。
版本化序列化/反
序列化以下列形式序列化您的对象状态:
<ObjectState version="1">
<Field1>value</Field1>
... etc ...
</ObjectState>
所以现在你有了生成保存状态的对象模型的版本。在您的反序列化中,您可以采取特殊的措施来适应这一事实。例如,将 Field1-Value 写入其他对象的列表中。
另一种方法是:
反序列化之前的版本化序列化和转换序列
化您的对象状态,如上所述(带有版本属性)。
反序列化时查看版本属性,如果这不是您期望的版本,则使用 xsl 脚本或 c# 代码将序列化对象状态转换为当前版本。您可以在当前项目中保存 xsl 转换列表
- conversions
- v1-v2
- v2-v3
如果您当前处于版本 3 并且想要加载您的 xml 文件,请查看版本属性并运行所有 xsl 脚本以获取您当前的版本(版本 3)。所以你会运行 xsl-script v1-v2,然后运行 v2-v3。
在这种情况下,您可以拥有不必关心向后功能的正常序列化和反序列化类。
使用数据绑定将对象模型绑定到 UI 是相当简单的。
如何在没有持久存储的情况下将对象与 GUI 控件联系起来?如果我手动执行,这意味着我必须为内存中的每个对象编写大量的代码。我已经为这些数据提供了某种类存储,但它不是绑定排序场景,就像在这里读这个写一样。
我是否应该编写一个加载器来加载序列化的 XML 并获取对象,然后读取对象并填充整个 GUI?显然这更像是手动加载而不是绑定。我错过了什么吗?
这是一篇关于如何使您的类或结构可序列化的精彩文章。我将创建一个类,允许您存储所需的所有数据。使类可序列化。这样,只需几行代码,您就可以将所有数据保存到文件中。然后只需多几行代码,您就可以从文件中检索数据。
序列化类的另一种方法是使用 ADO.NET 数据集进行数据存储,该数据集具有用于持久保存到 XML 文件的内置设施。代码将是最少的,您可以通过设计适合您正在执行的操作模型的表来仅存储您需要的相关数据。此外,如果您决定稍后将 UI 状态保存到数据库而不是本地文件,您将能够使用相同的代码。您只需要一个备用函数来保存数据集。