我的 WinClock 项目系列之四 (Memento 模式的应用)
动机 (Motivation)
在软件的构建过程中,某些对象的状态在转换过程中,可能由于某种需要,
要求程序能够回溯到对象之前某个点时的状态,如果使用一些公有接口来让
其他对象得到对象的状态,便会暴露对象的细节实现。
如何实现对象状态的良好保存与恢复?但同时又不会因此而破坏对象本身的
封装性。
意图 (Intent)
在不破坏封装性的前提下,捕获一个对象的内部状态,并在这个对象之外保存
这个状态。这样就可以将对象恢复到原先保存的状态。
《设计模式》———— GOF
从类图可以看出, ClockOption, MainForm, RemindData, RemindOparate 都实现了
一个 IMementoCapable 接口,这个接口定义如下:
很简单,只是能够把一个对象的状态保存为一个Properties对象,或者利用一个Properties
对象把这个对象的状态恢复到之前的状态。
这样做是很有必要的,比如对于 OptionForm,是需要可以预览的,这样用户在更改了一个
设置后马上就可以看到效果。比如拖动 Size 滑块,马上可以看到变大或者变小的效果。这样
的功能用到的还不少,使用 Memento 在窗体加载前先保存好原有的状态,在取消后就可以很
方便到恢复到之前的状态。下面是 Option 菜单执行的操作:
分钟检测一次是否有提醒任务。
属性的。主要是通过Get<T>,Set<T>两个泛型方法:
李建忠 C#面向对象设计模式纵横谈(21):(行为型模式) Memento 备忘录模式
SharpDevelop 3.0 源代码
在软件的构建过程中,某些对象的状态在转换过程中,可能由于某种需要,
要求程序能够回溯到对象之前某个点时的状态,如果使用一些公有接口来让
其他对象得到对象的状态,便会暴露对象的细节实现。
如何实现对象状态的良好保存与恢复?但同时又不会因此而破坏对象本身的
封装性。
意图 (Intent)
在不破坏封装性的前提下,捕获一个对象的内部状态,并在这个对象之外保存
这个状态。这样就可以将对象恢复到原先保存的状态。
《设计模式》———— GOF
从类图可以看出, ClockOption, MainForm, RemindData, RemindOparate 都实现了
一个 IMementoCapable 接口,这个接口定义如下:
1// IMementoCapable.cs
2public interface IMementoCapable {
3 Properties CreateMemento();
4 void SetMemento(Properties properties);
5}
2public interface IMementoCapable {
3 Properties CreateMemento();
4 void SetMemento(Properties properties);
5}
很简单,只是能够把一个对象的状态保存为一个Properties对象,或者利用一个Properties
对象把这个对象的状态恢复到之前的状态。
这样做是很有必要的,比如对于 OptionForm,是需要可以预览的,这样用户在更改了一个
设置后马上就可以看到效果。比如拖动 Size 滑块,马上可以看到变大或者变小的效果。这样
的功能用到的还不少,使用 Memento 在窗体加载前先保存好原有的状态,在取消后就可以很
方便到恢复到之前的状态。下面是 Option 菜单执行的操作:
1// OptionElement.cs
2internal class OptionElement : Element {
3 public OptionElement(Mediator mediator, ToolStripMenuItem source)
4 : base(mediator) {
5 base.command = new MenuItemCommand(source);
6 }
7
8 protected override void OnExecute() {
9 using (OptionForm optionForm = new OptionForm(base.mainForm)) {
10 Core.Properties memento = mainForm.CreateMemento();
11 byte previewOpacity = clockOpt.PreviewOpacity;
12 if (optionForm.ShowDialog() == DialogResult.Cancel) {
13 mainForm.SetMemento(memento);
14 base.clockOpt.PreviewOpacity = previewOpacity;
15 mainForm.RefreshSkin();
16 } else { // OK clicked
17 clockOpt.PreviewOpacity = clockOpt.Opacity;
18 Point location = mainForm.Location;
19 mainForm.CheckBounds(ref location);
20 mainForm.Location = location;
21 mainForm.RefreshWindow();
22 }
23 }
24 }
25}
下面是 Remind 菜单要执行的操作:2internal class OptionElement : Element {
3 public OptionElement(Mediator mediator, ToolStripMenuItem source)
4 : base(mediator) {
5 base.command = new MenuItemCommand(source);
6 }
7
8 protected override void OnExecute() {
9 using (OptionForm optionForm = new OptionForm(base.mainForm)) {
10 Core.Properties memento = mainForm.CreateMemento();
11 byte previewOpacity = clockOpt.PreviewOpacity;
12 if (optionForm.ShowDialog() == DialogResult.Cancel) {
13 mainForm.SetMemento(memento);
14 base.clockOpt.PreviewOpacity = previewOpacity;
15 mainForm.RefreshSkin();
16 } else { // OK clicked
17 clockOpt.PreviewOpacity = clockOpt.Opacity;
18 Point location = mainForm.Location;
19 mainForm.CheckBounds(ref location);
20 mainForm.Location = location;
21 mainForm.RefreshWindow();
22 }
23 }
24 }
25}
1// RemindElement.cs
2internal class RemindElement : Element {
3 private Timer timerRemind;
4
5 public RemindElement(Mediator mediator, ToolStripMenuItem source)
6 : base(mediator) {
7 base.command = new MenuItemCommand(source);
8 timerRemind = new Timer();
9 this.timerRemind.Interval = 60000;
10 this.timerRemind.Tick += TimerRemindOnTick;
11 this.timerRemind.Enabled = clockOpt.HaveRemind;
12 }
13
14 protected override void OnExecute() {
15 using (RemindListForm frmRemind = new RemindListForm(remindOperate)) {
16 Core.Properties savedProperties = remindOperate.CreateMemento();
17 if (frmRemind.ShowDialog() == DialogResult.OK) {
18 Core.Properties properties = remindOperate.CreateMemento();
19 PropertyService.Set("WinClock.RemindOperate", properties);
20 clockOpt.HaveRemind = frmRemind.HaveRemind;
21 } else { // User Canceled
22 remindOperate.SetMemento(savedProperties);
23 }
24 }
25 }
26
27 protected internal override void OnStatusChanged() {
28 if (clockOpt.HaveRemind) {
29 if (!this.timerRemind.Enabled) {
30 this.timerRemind.Start();
31 }
32 } else {
33 if (!this.timerRemind.Enabled) {
34 this.timerRemind.Stop();
35 }
36 }
37 }
38
39 protected override void Dispose(bool disposing) {
40 if (disposing) {
41 try {
42 timerRemind.Tick -= TimerRemindOnTick;
43 timerRemind.Dispose();
44 } finally {
45 timerRemind = null;
46 }
47 }
48
49 base.Dispose(disposing);
50 }
51
52 private void TimerRemindOnTick(object sender, EventArgs e) {
53 remindOperate.CheckRemindList();
54 }
55}
RemindOperate 类实现的是对定时提醒任务的检测和保存,它利用一个 Timer, 每隔一2internal class RemindElement : Element {
3 private Timer timerRemind;
4
5 public RemindElement(Mediator mediator, ToolStripMenuItem source)
6 : base(mediator) {
7 base.command = new MenuItemCommand(source);
8 timerRemind = new Timer();
9 this.timerRemind.Interval = 60000;
10 this.timerRemind.Tick += TimerRemindOnTick;
11 this.timerRemind.Enabled = clockOpt.HaveRemind;
12 }
13
14 protected override void OnExecute() {
15 using (RemindListForm frmRemind = new RemindListForm(remindOperate)) {
16 Core.Properties savedProperties = remindOperate.CreateMemento();
17 if (frmRemind.ShowDialog() == DialogResult.OK) {
18 Core.Properties properties = remindOperate.CreateMemento();
19 PropertyService.Set("WinClock.RemindOperate", properties);
20 clockOpt.HaveRemind = frmRemind.HaveRemind;
21 } else { // User Canceled
22 remindOperate.SetMemento(savedProperties);
23 }
24 }
25 }
26
27 protected internal override void OnStatusChanged() {
28 if (clockOpt.HaveRemind) {
29 if (!this.timerRemind.Enabled) {
30 this.timerRemind.Start();
31 }
32 } else {
33 if (!this.timerRemind.Enabled) {
34 this.timerRemind.Stop();
35 }
36 }
37 }
38
39 protected override void Dispose(bool disposing) {
40 if (disposing) {
41 try {
42 timerRemind.Tick -= TimerRemindOnTick;
43 timerRemind.Dispose();
44 } finally {
45 timerRemind = null;
46 }
47 }
48
49 base.Dispose(disposing);
50 }
51
52 private void TimerRemindOnTick(object sender, EventArgs e) {
53 remindOperate.CheckRemindList();
54 }
55}
分钟检测一次是否有提醒任务。
1// RemindOperate.cs
2[Serializable()]
3public struct RemindData : IMementoCapable {
4 public string remindTitle;
5 public string remindMode;
6 public string remindTime;
7 public string remindText;
8 public string programPath;
9 public string musicPath;
10 public bool showMessage;
11 public bool executeprogram;
12 public bool playSound;
13 public bool closeComputer;
14
15 IMementoCapable Members
47}
48
49[Serializable()]
50public class RemindOperate : IMementoCapable, ILacalizable {
51 private List<RemindData> remindDataList;
52 public static readonly string YearMonthDayTimeFmt = "yyyy-MM-dd HH:mm";
53 public static readonly string YearMonthDayFmt = "yyyy-MM-dd";
54 public static readonly string MonthDayFmt = "MM-dd";
55 public static readonly string DayFmt = "dd";
56 public static readonly string TimeFmt = "HH:mm";
57
58 public static readonly Dictionary<DayOfWeek, string> Days;
59 static RemindOperate() {
60 Days = new Dictionary<DayOfWeek, string>();
61 Initialize();
62 }
63
64 public RemindOperate() {
65 remindDataList = new List<RemindData>();
66 ResourceService.LocalizeList.Add(this);
67 }
68
69 public List<RemindData> RemindDataList {
70 get {
71 return this.remindDataList;
72 }
73 }
74
75 public void CheckRemindList() {
76 string currentTime = DateTime.Now.ToString(YearMonthDayTimeFmt);
77 currentTime += " " + Days[DateTime.Now.DayOfWeek];
78 for (int index = 0; index < remindDataList.Count; ++index) {
79 RemindData rmdData = remindDataList[index];
80 string remindTime = rmdData.remindTime;
81
82 if (currentTime.IndexOf(remindTime) != -1) {
83 ShowRemindInfo(index);
84 }
85 }
86 }
87
88 public void ShowRemindInfo(int index) {
89 RemindData rmdData = remindDataList[index];
90 if (rmdData.closeComputer) {
91 MessageService.ShowShutdownMessage();
92 } else {
93 if (rmdData.playSound) {
94 SoundPlayer soundPlayer = new SoundPlayer(rmdData.musicPath);
95 soundPlayer.Play();
96 }
97
98 if (rmdData.showMessage) {
99 MessageService.ShowRemindMessage(rmdData);
100 }
101
102 if (rmdData.executeprogram) {
103 Process proc = new Process();
104 proc.StartInfo.FileName = rmdData.programPath;
105 proc.StartInfo.Arguments = string.Empty;
106 proc.Start();
107 }
108 }
109 }
110
111 private static void Initialize() {
112 Days.Clear();
113 Days.Add(DayOfWeek.Sunday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Sunday"));
114 Days.Add(DayOfWeek.Monday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Monday"));
115 Days.Add(DayOfWeek.Tuesday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Tuesday"));
116 Days.Add(DayOfWeek.Wednesday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Wednesday"));
117 Days.Add(DayOfWeek.Thursday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Thursday"));
118 Days.Add(DayOfWeek.Friday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Friday"));
119 Days.Add(DayOfWeek.Saturday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Saturday"));
120 }
121
122 IMementoCapable Members
148
149 ILacalizable Members
156}
在这个软件中,Properties类是Memento的关键,同时也是以后要讲的持久化存储的关键。这里只要看他是如何设置和取得一个2[Serializable()]
3public struct RemindData : IMementoCapable {
4 public string remindTitle;
5 public string remindMode;
6 public string remindTime;
7 public string remindText;
8 public string programPath;
9 public string musicPath;
10 public bool showMessage;
11 public bool executeprogram;
12 public bool playSound;
13 public bool closeComputer;
14
15 IMementoCapable Members
47}
48
49[Serializable()]
50public class RemindOperate : IMementoCapable, ILacalizable {
51 private List<RemindData> remindDataList;
52 public static readonly string YearMonthDayTimeFmt = "yyyy-MM-dd HH:mm";
53 public static readonly string YearMonthDayFmt = "yyyy-MM-dd";
54 public static readonly string MonthDayFmt = "MM-dd";
55 public static readonly string DayFmt = "dd";
56 public static readonly string TimeFmt = "HH:mm";
57
58 public static readonly Dictionary<DayOfWeek, string> Days;
59 static RemindOperate() {
60 Days = new Dictionary<DayOfWeek, string>();
61 Initialize();
62 }
63
64 public RemindOperate() {
65 remindDataList = new List<RemindData>();
66 ResourceService.LocalizeList.Add(this);
67 }
68
69 public List<RemindData> RemindDataList {
70 get {
71 return this.remindDataList;
72 }
73 }
74
75 public void CheckRemindList() {
76 string currentTime = DateTime.Now.ToString(YearMonthDayTimeFmt);
77 currentTime += " " + Days[DateTime.Now.DayOfWeek];
78 for (int index = 0; index < remindDataList.Count; ++index) {
79 RemindData rmdData = remindDataList[index];
80 string remindTime = rmdData.remindTime;
81
82 if (currentTime.IndexOf(remindTime) != -1) {
83 ShowRemindInfo(index);
84 }
85 }
86 }
87
88 public void ShowRemindInfo(int index) {
89 RemindData rmdData = remindDataList[index];
90 if (rmdData.closeComputer) {
91 MessageService.ShowShutdownMessage();
92 } else {
93 if (rmdData.playSound) {
94 SoundPlayer soundPlayer = new SoundPlayer(rmdData.musicPath);
95 soundPlayer.Play();
96 }
97
98 if (rmdData.showMessage) {
99 MessageService.ShowRemindMessage(rmdData);
100 }
101
102 if (rmdData.executeprogram) {
103 Process proc = new Process();
104 proc.StartInfo.FileName = rmdData.programPath;
105 proc.StartInfo.Arguments = string.Empty;
106 proc.Start();
107 }
108 }
109 }
110
111 private static void Initialize() {
112 Days.Clear();
113 Days.Add(DayOfWeek.Sunday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Sunday"));
114 Days.Add(DayOfWeek.Monday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Monday"));
115 Days.Add(DayOfWeek.Tuesday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Tuesday"));
116 Days.Add(DayOfWeek.Wednesday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Wednesday"));
117 Days.Add(DayOfWeek.Thursday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Thursday"));
118 Days.Add(DayOfWeek.Friday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Friday"));
119 Days.Add(DayOfWeek.Saturday, ResourceService.GetString("CSharpCode.Core.RemindOperate.Saturday"));
120 }
121
122 IMementoCapable Members
148
149 ILacalizable Members
156}
属性的。主要是通过Get<T>,Set<T>两个泛型方法:
1// Part of Properties.cs
2public class Properties {
3 Dictionary<string, object> properties = new Dictionary<string, object>();
4 protected EventHandlerList Events = new EventHandlerList();
5 private static readonly object EventPropertiesChanged = new object();
6
7 public string this[string property] {
8 get {
9 return Convert.ToString(Get(property));
10 }
11 set {
12 Set(property, value);
13 }
14 }
15
16 public event EventHandler<PropertyChangedEventArgs> PropertyChanged {
17 add {
18 this.Events.AddHandler(EventPropertiesChanged, value);
19 }
20 remove {
21 this.Events.RemoveHandler(EventPropertiesChanged, value);
22 }
23 }
24
25 public string[] Elements {
26 get {
27 lock (properties) {
28 List<string> ret = new List<string>();
29 foreach (KeyValuePair<string, object> property in properties) {
30 ret.Add(property.Key);
31 }
32 return ret.ToArray();
33 }
34 }
35 }
36
37 public object Get(string property) {
38 lock (properties) {
39 object val;
40 properties.TryGetValue(property, out val);
41 return val;
42 }
43 }
44
45 public void Set<T>(string property, T value) {
46 T oldValue = default(T);
47 lock (properties) {
48 if (!properties.ContainsKey(property)) {
49 properties.Add(property, value);
50 } else {
51 oldValue = Get<T>(property, value);
52 properties[property] = value;
53 }
54 }
55 OnPropertyChanged(new PropertyChangedEventArgs(this, property, oldValue, value));
56 }
57
58 public bool Contains(string property) {
59 lock (properties) {
60 return properties.ContainsKey(property);
61 }
62 }
63
64 public int Count {
65 get {
66 lock (properties) {
67 return properties.Count;
68 }
69 }
70 }
71
72 public bool Remove(string property) {
73 lock (properties) {
74 return properties.Remove(property);
75 }
76 }
77
78 public override string ToString() {
79 lock (properties) {
80 StringBuilder sb = new StringBuilder();
81 sb.Append("[Properties:{");
82 foreach (KeyValuePair<string, object> entry in properties) {
83 sb.Append(entry.Key);
84 sb.Append("=");
85 sb.Append(entry.Value);
86 sb.Append(",");
87 }
88 sb.Append("}]");
89 return sb.ToString();
90 }
91 }
92
93 public T Get<T>(string property, T defaultValue) {
94 lock (properties) {
95 object o;
96 if (!properties.TryGetValue(property, out o)) {
97 properties.Add(property, defaultValue);
98 return defaultValue;
99 }
100
101 if (o is string && typeof(T) != typeof(string)) {
102 TypeConverter c = TypeDescriptor.GetConverter(typeof(T));
103 try {
104 o = c.ConvertFromInvariantString(o.ToString());
105 } catch (Exception ex) {
106 MessageBox.Show("Error loading property '" + property + "': " + ex.Message);
107 o = defaultValue;
108 }
109 properties[property] = o; // store for future look up
110 } else if (o is ArrayList && typeof(T).IsArray) {
111 ArrayList list = (ArrayList)o;
112 Type elementType = typeof(T).GetElementType();
113 Array arr = System.Array.CreateInstance(elementType, list.Count);
114 TypeConverter c = TypeDescriptor.GetConverter(elementType);
115 try {
116 for (int i = 0; i < arr.Length; ++i) {
117 if (list[i] != null) {
118 arr.SetValue(c.ConvertFromInvariantString(list[i].ToString()), i);
119 }
120 }
121 o = arr;
122 } catch (Exception ex) {
123 MessageBox.Show("Error loading property '" + property + "': " + ex.Message);
124 o = defaultValue;
125 }
126 properties[property] = o; // store for future look up
127 } else if (!(o is string) && typeof(T) == typeof(string)) {
128 TypeConverter c = TypeDescriptor.GetConverter(typeof(T));
129 if (c.CanConvertTo(typeof(string))) {
130 o = c.ConvertToInvariantString(o);
131 } else {
132 o = o.ToString();
133 }
134 }
135 try {
136 return (T)o;
137 } catch (NullReferenceException) {
138 // can happen when configuration is invalid -> o is null and a value type is expected
139 return defaultValue;
140 }
141 }
142 }
143
144 protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) {
145 EventHandler<PropertyChangedEventArgs> handler = this.Events[EventPropertiesChanged] as EventHandler<PropertyChangedEventArgs>;
146 if (handler != null) {
147 handler(this, e);
148 }
149 }
150
151 // Other methods
152}
参考资料:2public class Properties {
3 Dictionary<string, object> properties = new Dictionary<string, object>();
4 protected EventHandlerList Events = new EventHandlerList();
5 private static readonly object EventPropertiesChanged = new object();
6
7 public string this[string property] {
8 get {
9 return Convert.ToString(Get(property));
10 }
11 set {
12 Set(property, value);
13 }
14 }
15
16 public event EventHandler<PropertyChangedEventArgs> PropertyChanged {
17 add {
18 this.Events.AddHandler(EventPropertiesChanged, value);
19 }
20 remove {
21 this.Events.RemoveHandler(EventPropertiesChanged, value);
22 }
23 }
24
25 public string[] Elements {
26 get {
27 lock (properties) {
28 List<string> ret = new List<string>();
29 foreach (KeyValuePair<string, object> property in properties) {
30 ret.Add(property.Key);
31 }
32 return ret.ToArray();
33 }
34 }
35 }
36
37 public object Get(string property) {
38 lock (properties) {
39 object val;
40 properties.TryGetValue(property, out val);
41 return val;
42 }
43 }
44
45 public void Set<T>(string property, T value) {
46 T oldValue = default(T);
47 lock (properties) {
48 if (!properties.ContainsKey(property)) {
49 properties.Add(property, value);
50 } else {
51 oldValue = Get<T>(property, value);
52 properties[property] = value;
53 }
54 }
55 OnPropertyChanged(new PropertyChangedEventArgs(this, property, oldValue, value));
56 }
57
58 public bool Contains(string property) {
59 lock (properties) {
60 return properties.ContainsKey(property);
61 }
62 }
63
64 public int Count {
65 get {
66 lock (properties) {
67 return properties.Count;
68 }
69 }
70 }
71
72 public bool Remove(string property) {
73 lock (properties) {
74 return properties.Remove(property);
75 }
76 }
77
78 public override string ToString() {
79 lock (properties) {
80 StringBuilder sb = new StringBuilder();
81 sb.Append("[Properties:{");
82 foreach (KeyValuePair<string, object> entry in properties) {
83 sb.Append(entry.Key);
84 sb.Append("=");
85 sb.Append(entry.Value);
86 sb.Append(",");
87 }
88 sb.Append("}]");
89 return sb.ToString();
90 }
91 }
92
93 public T Get<T>(string property, T defaultValue) {
94 lock (properties) {
95 object o;
96 if (!properties.TryGetValue(property, out o)) {
97 properties.Add(property, defaultValue);
98 return defaultValue;
99 }
100
101 if (o is string && typeof(T) != typeof(string)) {
102 TypeConverter c = TypeDescriptor.GetConverter(typeof(T));
103 try {
104 o = c.ConvertFromInvariantString(o.ToString());
105 } catch (Exception ex) {
106 MessageBox.Show("Error loading property '" + property + "': " + ex.Message);
107 o = defaultValue;
108 }
109 properties[property] = o; // store for future look up
110 } else if (o is ArrayList && typeof(T).IsArray) {
111 ArrayList list = (ArrayList)o;
112 Type elementType = typeof(T).GetElementType();
113 Array arr = System.Array.CreateInstance(elementType, list.Count);
114 TypeConverter c = TypeDescriptor.GetConverter(elementType);
115 try {
116 for (int i = 0; i < arr.Length; ++i) {
117 if (list[i] != null) {
118 arr.SetValue(c.ConvertFromInvariantString(list[i].ToString()), i);
119 }
120 }
121 o = arr;
122 } catch (Exception ex) {
123 MessageBox.Show("Error loading property '" + property + "': " + ex.Message);
124 o = defaultValue;
125 }
126 properties[property] = o; // store for future look up
127 } else if (!(o is string) && typeof(T) == typeof(string)) {
128 TypeConverter c = TypeDescriptor.GetConverter(typeof(T));
129 if (c.CanConvertTo(typeof(string))) {
130 o = c.ConvertToInvariantString(o);
131 } else {
132 o = o.ToString();
133 }
134 }
135 try {
136 return (T)o;
137 } catch (NullReferenceException) {
138 // can happen when configuration is invalid -> o is null and a value type is expected
139 return defaultValue;
140 }
141 }
142 }
143
144 protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) {
145 EventHandler<PropertyChangedEventArgs> handler = this.Events[EventPropertiesChanged] as EventHandler<PropertyChangedEventArgs>;
146 if (handler != null) {
147 handler(this, e);
148 }
149 }
150
151 // Other methods
152}
李建忠 C#面向对象设计模式纵横谈(21):(行为型模式) Memento 备忘录模式
SharpDevelop 3.0 源代码
这是微软技术的一贯特点,使用简单。但是如果要深入的话,还是要投入不少精力的