##// END OF EJS Templates
runnable component, work in progress
cin -
r185:822aab37b107 ref20160224
parent child
Show More
@@ -0,0 +1,194
1 using System;
2 using System.Reflection;
3 using System.Threading;
4 using Implab.Parallels;
5 using Implab.Components;
6
7 #if MONO
8
9 using NUnit.Framework;
10 using TestClassAttribute = NUnit.Framework.TestFixtureAttribute;
11 using TestMethodAttribute = NUnit.Framework.TestAttribute;
12
13 #else
14
15 using Microsoft.VisualStudio.TestTools.UnitTesting;
16
17 #endif
18
19 namespace Implab.Test {
20 [TestClass]
21 public class RunnableComponentTests {
22
23 static void ShouldThrow(Action action) {
24 try {
25 action();
26 Assert.Fail();
27 } catch(AssertionException) {
28 throw;
29 } catch {
30 }
31 }
32
33 class Runnable : RunnableComponent {
34 public Runnable(bool initialized) : base(initialized) {
35 }
36
37 public Action MockInit {
38 get;
39 set;
40 }
41
42 public Func<IPromise> MockStart {
43 get;
44 set;
45 }
46
47 public Func<IPromise> MockStop {
48 get;
49 set;
50 }
51
52 protected override IPromise OnStart() {
53 return MockStart != null ? MockStart() : base.OnStart();
54 }
55
56 protected override IPromise OnStop() {
57 return MockStop != null ? MockStop() : base.OnStart();
58 }
59
60 protected override void OnInitialize() {
61 if (MockInit != null)
62 MockInit();
63 }
64 }
65
66 [TestMethod]
67 public void NormalFlowTest() {
68 var comp = new Runnable(false);
69
70 Assert.AreEqual(ExecutionState.Created, comp.State);
71
72 comp.Init();
73
74 Assert.AreEqual(ExecutionState.Ready, comp.State);
75
76 comp.Start().Join(1000);
77
78 Assert.AreEqual(ExecutionState.Running, comp.State);
79
80 comp.Stop().Join(1000);
81
82 Assert.AreEqual(ExecutionState.Disposed, comp.State);
83
84 }
85
86 [TestMethod]
87 public void InitFailTest() {
88 var comp = new Runnable(false) {
89 MockInit = () => {
90 throw new Exception("BAD");
91 }
92 };
93
94 ShouldThrow(() => comp.Start());
95 ShouldThrow(() => comp.Stop());
96 Assert.AreEqual(ExecutionState.Created, comp.State);
97
98 ShouldThrow(comp.Init);
99
100 Assert.AreEqual(ExecutionState.Failed, comp.State);
101
102 ShouldThrow(() => comp.Start());
103 ShouldThrow(() => comp.Stop());
104 Assert.AreEqual(ExecutionState.Failed, comp.State);
105
106 comp.Dispose();
107 Assert.AreEqual(ExecutionState.Disposed, comp.State);
108 }
109
110 [TestMethod]
111 public void DisposedTest() {
112
113 var comp = new Runnable(false);
114 comp.Dispose();
115
116 ShouldThrow(() => comp.Start());
117 ShouldThrow(() => comp.Stop());
118 ShouldThrow(comp.Init);
119
120 Assert.AreEqual(ExecutionState.Disposed, comp.State);
121 }
122
123 [TestMethod]
124 public void StartCancelTest() {
125 var comp = new Runnable(true) {
126 MockStart = () => PromiseHelper.Sleep(100000, 0)
127 };
128
129 var p = comp.Start();
130 Assert.AreEqual(ExecutionState.Starting, comp.State);
131 p.Cancel();
132 ShouldThrow(() => p.Join(1000));
133 Assert.AreEqual(ExecutionState.Failed, comp.State);
134 Assert.IsInstanceOfType(typeof(OperationCanceledException), comp.LastError);
135
136 comp.Dispose();
137 }
138
139 [TestMethod]
140 public void StartStopTest() {
141 var stop = new Signal();
142 var comp = new Runnable(true) {
143 MockStart = () => PromiseHelper.Sleep(100000, 0),
144 MockStop = () => AsyncPool.RunThread(stop.Wait)
145 };
146
147 var p1 = comp.Start();
148 var p2 = comp.Stop();
149 // should enter stopping state
150
151 ShouldThrow(p1.Join);
152 Assert.IsTrue(p1.IsCancelled);
153 Assert.AreEqual(ExecutionState.Stopping, comp.State);
154
155 stop.Set();
156 p2.Join(1000);
157 Assert.AreEqual(ExecutionState.Disposed, comp.State);
158 }
159
160 [TestMethod]
161 public void StartStopFailTest() {
162 var comp = new Runnable(true) {
163 MockStart = () => PromiseHelper.Sleep(100000, 0).Then(null,null,x => { throw new Exception("I'm dead"); })
164 };
165
166 comp.Start();
167 var p = comp.Stop();
168 // if Start fails to cancel, should fail to stop
169 ShouldThrow(() => p.Join(1000));
170 Assert.AreEqual(ExecutionState.Failed, comp.State);
171 Assert.IsNotNull(comp.LastError);
172 Assert.AreEqual("I'm dead", comp.LastError.Message);
173 }
174
175 [TestMethod]
176 public void StopCancelTest() {
177 var comp = new Runnable(true) {
178 MockStop = () => PromiseHelper.Sleep(100000, 0)
179 };
180
181 comp.Start();
182 var p = comp.Stop();
183 Assert.AreEqual(ExecutionState.Stopping, comp.State);
184 p.Cancel();
185 ShouldThrow(() => p.Join(1000));
186 Assert.AreEqual(ExecutionState.Failed, comp.State);
187 Assert.IsInstanceOfType(typeof(OperationCanceledException), comp.LastError);
188
189 comp.Dispose();
190 }
191
192 }
193 }
194
@@ -58,6 +58,7
58 <Compile Include="PromiseHelper.cs" />
58 <Compile Include="PromiseHelper.cs" />
59 <Compile Include="Properties\AssemblyInfo.cs" />
59 <Compile Include="Properties\AssemblyInfo.cs" />
60 <Compile Include="CancelationTests.cs" />
60 <Compile Include="CancelationTests.cs" />
61 <Compile Include="RunnableComponentTests.cs" />
61 </ItemGroup>
62 </ItemGroup>
62 <ItemGroup>
63 <ItemGroup>
63 <ProjectReference Include="..\Implab\Implab.csproj">
64 <ProjectReference Include="..\Implab\Implab.csproj">
@@ -77,18 +77,19 namespace Implab {
77 /// <param name="error">Π˜ΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ возникшСС ΠΏΡ€ΠΈ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠΈ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ</param>
77 /// <param name="error">Π˜ΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ возникшСС ΠΏΡ€ΠΈ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠΈ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ</param>
78 /// <exception cref="InvalidOperationException">Π”Π°Π½Π½ΠΎΠ΅ ΠΎΠ±Π΅Ρ‰Π°Π½ΠΈΠ΅ ΡƒΠΆΠ΅ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΎ</exception>
78 /// <exception cref="InvalidOperationException">Π”Π°Π½Π½ΠΎΠ΅ ΠΎΠ±Π΅Ρ‰Π°Π½ΠΈΠ΅ ΡƒΠΆΠ΅ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΎ</exception>
79 protected void SetError(Exception error) {
79 protected void SetError(Exception error) {
80 while (error is PromiseTransientException)
81 error = error.InnerException;
82
83 var isCancel = error is OperationCanceledException;
84
80 if (BeginTransit()) {
85 if (BeginTransit()) {
81 if (error is OperationCanceledException) {
86 m_error = isCancel ? error.InnerException : error;
82 m_error = error.InnerException;
87 CompleteTransit(isCancel ? CANCELLED_STATE : REJECTED_STATE);
83 CompleteTransit(CANCELLED_STATE);
88
84 } else {
85 m_error = error is PromiseTransientException ? error.InnerException : error;
86 CompleteTransit(REJECTED_STATE);
87 }
88 Signal();
89 Signal();
89 } else {
90 } else {
90 WaitTransition();
91 WaitTransition();
91 if (m_state == SUCCEEDED_STATE)
92 if (!isCancel || m_state == SUCCEEDED_STATE)
92 throw new InvalidOperationException("The promise is already resolved");
93 throw new InvalidOperationException("The promise is already resolved");
93 }
94 }
94 }
95 }
@@ -4,6 +4,15 namespace Implab {
4 public class ActionChainTask : ActionChainTaskBase, IDeferred {
4 public class ActionChainTask : ActionChainTaskBase, IDeferred {
5 readonly Func<IPromise> m_task;
5 readonly Func<IPromise> m_task;
6
6
7 /// <summary>
8 /// Initializes a new instance of the <see cref="Implab.ActionChainTask"/> class.
9 /// </summary>
10 /// <param name="task">The operation which will be performed when the <see cref="Resolve()"/> is called.</param>
11 /// <param name="error">The error handler which will invoke when the <see cref="Reject(Exception)"/> is called or when the task fails with an error.</param>
12 /// <param name="cancel">The cancellation handler.</param>
13 /// <param name="autoCancellable">If set to <c>true</c> will automatically accept
14 /// all cancel requests before the task is started with <see cref="Resolve()"/>,
15 /// after that all requests are directed to the task.</param>
7 public ActionChainTask(Func<IPromise> task, Func<Exception, IPromise> error, Func<Exception, IPromise> cancel, bool autoCancellable) : base(error,cancel, autoCancellable) {
16 public ActionChainTask(Func<IPromise> task, Func<Exception, IPromise> error, Func<Exception, IPromise> cancel, bool autoCancellable) : base(error,cancel, autoCancellable) {
8 m_task = task;
17 m_task = task;
9 }
18 }
@@ -1,8 +1,7
1 using System;
1 using System;
2 using Implab.Formats;
3
2
4 namespace Implab.Components {
3 namespace Implab.Components {
5 public class RunnableComponent : Disposable, IRunnable, IInitializable {
4 public abstract class RunnableComponent : IDisposable, IRunnable, IInitializable {
6 enum Commands {
5 enum Commands {
7 Ok = 0,
6 Ok = 0,
8 Fail,
7 Fail,
@@ -19,8 +18,11 namespace Implab.Components {
19 static StateMachine() {
18 static StateMachine() {
20 _transitions = new ExecutionState[(int)ExecutionState.Last + 1, (int)Commands.Last + 1];
19 _transitions = new ExecutionState[(int)ExecutionState.Last + 1, (int)Commands.Last + 1];
21
20
22 Edge(ExecutionState.Created, ExecutionState.Ready, Commands.Ok);
21 Edge(ExecutionState.Created, ExecutionState.Initializing, Commands.Init);
23 Edge(ExecutionState.Created, ExecutionState.Failed, Commands.Fail);
22 Edge(ExecutionState.Created, ExecutionState.Disposed, Commands.Dispose);
23
24 Edge(ExecutionState.Initializing, ExecutionState.Ready, Commands.Ok);
25 Edge(ExecutionState.Initializing, ExecutionState.Failed, Commands.Fail);
24
26
25 Edge(ExecutionState.Ready, ExecutionState.Starting, Commands.Start);
27 Edge(ExecutionState.Ready, ExecutionState.Starting, Commands.Start);
26 Edge(ExecutionState.Ready, ExecutionState.Disposed, Commands.Dispose);
28 Edge(ExecutionState.Ready, ExecutionState.Disposed, Commands.Dispose);
@@ -36,7 +38,8 namespace Implab.Components {
36
38
37 Edge(ExecutionState.Stopping, ExecutionState.Failed, Commands.Fail);
39 Edge(ExecutionState.Stopping, ExecutionState.Failed, Commands.Fail);
38 Edge(ExecutionState.Stopping, ExecutionState.Disposed, Commands.Ok);
40 Edge(ExecutionState.Stopping, ExecutionState.Disposed, Commands.Ok);
39 Edge(ExecutionState.Stopping, ExecutionState.Disposed, Commands.Dispose);
41
42 Edge(ExecutionState.Failed, ExecutionState.Disposed, Commands.Dispose);
40 }
43 }
41
44
42 static void Edge(ExecutionState s1, ExecutionState s2, Commands cmd) {
45 static void Edge(ExecutionState s1, ExecutionState s2, Commands cmd) {
@@ -70,72 +73,93 namespace Implab.Components {
70 m_stateMachine = new StateMachine(initialized ? ExecutionState.Ready : ExecutionState.Created);
73 m_stateMachine = new StateMachine(initialized ? ExecutionState.Ready : ExecutionState.Created);
71 }
74 }
72
75
76 protected virtual int DisposeTimeout {
77 get {
78 return 10000;
79 }
80 }
81
73 void ThrowInvalidCommand(Commands cmd) {
82 void ThrowInvalidCommand(Commands cmd) {
83 if (m_stateMachine.State == ExecutionState.Disposed)
84 throw new ObjectDisposedException(ToString());
85
74 throw new InvalidOperationException(String.Format("Commnd {0} is not allowed in the state {1}", cmd, m_stateMachine.State));
86 throw new InvalidOperationException(String.Format("Commnd {0} is not allowed in the state {1}", cmd, m_stateMachine.State));
75 }
87 }
76
88
77 protected void Move(Commands cmd) {
89 void Move(Commands cmd) {
78 lock (m_stateMachine)
90 if (!m_stateMachine.Move(cmd))
79 if (!m_stateMachine.Move(cmd))
91 ThrowInvalidCommand(cmd);
80 ThrowInvalidCommand(cmd);
81 }
92 }
82
93
83 protected void Fail(Exception err) {
94 void Invoke(Commands cmd, Action action) {
84 lock (m_stateMachine) {
95 lock (m_stateMachine)
85 if (!m_stateMachine.Move(Commands.Fail))
96 Move(cmd);
86 ThrowInvalidCommand(Commands.Fail);
97
87
88 m_lastError = err;
89 }
90 }
91
92 protected void Success() {
93 Move(Commands.Ok);
94 }
95
96 protected void Invoke(Commands cmd, Action action) {
97 Move(cmd);
98 try {
98 try {
99 action();
99 action();
100 Move(Commands.Ok);
100 lock(m_stateMachine)
101 Move(Commands.Ok);
102
101 } catch (Exception err) {
103 } catch (Exception err) {
102 Fail(err);
104 lock (m_stateMachine) {
105 Move(Commands.Fail);
106 m_lastError = err;
107 }
103 throw;
108 throw;
104 }
109 }
105 }
110 }
106
111
107 protected IPromise InvokeAsync(Commands cmd, Func<IPromise> action) {
112 IPromise InvokeAsync(Commands cmd, Func<IPromise> action, Action<IPromise, IDeferred> chain) {
108 Move(cmd);
113 IPromise promise = null;
109 var medium = new Promise();
114 IPromise prev;
110
115
111 IPromise promise = null;
116 var task = new ActionChainTask(action, null, null, true);
117
118 lock (m_stateMachine) {
119 Move(cmd);
120
121 prev = m_pending;
112
122
113 promise = medium.Then(
123 promise = task.Then(
114 () => {
124 () => {
115 lock(m_stateMachine) {
125 lock(m_stateMachine) {
116 if (m_pending == promise) {
126 if (m_pending == promise) {
117 m_pending = null;
127 Move(Commands.Ok);
118 Move(Commands.Ok);
128 m_pending = null;
129 }
119 }
130 }
131 }, e => {
132 lock(m_stateMachine) {
133 if (m_pending == promise) {
134 Move(Commands.Fail);
135 m_pending = null;
136 m_lastError = e;
137 }
138 }
139 throw new PromiseTransientException(e);
140 },
141 r => {
142 lock(m_stateMachine) {
143 if (m_pending == promise) {
144 Move(Commands.Fail);
145 m_pending = null;
146 m_lastError = new OperationCanceledException("The operation has been cancelled", r);
147 }
148
149 }
150 throw new OperationCanceledException("The operation has been cancelled", r);
120 }
151 }
121 }, e => {
152 );
122 if (m_pending == promise) {
123 m_pending = null;
124 Fail(
125 }
126 }
127 );
128
153
154 m_pending = promise;
155 }
129
156
157 if (prev == null)
158 task.Resolve();
159 else
160 chain(prev, task);
130
161
131 return Safe.InvokePromise(action).Then(
162 return promise;
132 Success,
133 Fail
134 );
135 }
136
137 void AddPending(IPromise result) {
138
139 }
163 }
140
164
141
165
@@ -153,43 +177,86 namespace Implab.Components {
153 #region IRunnable implementation
177 #region IRunnable implementation
154
178
155 public IPromise Start() {
179 public IPromise Start() {
156 Move(Commands.Start);
180 return InvokeAsync(Commands.Start, OnStart, null);
157
158 return Safe.InvokePromise(OnStart).Then(
159 () => {
160 Move(Commands.Ok);
161 Run();
162 },
163 () => {
164 Move(Commands.Fail);
165 }
166 );
167 }
181 }
168
182
169 protected virtual IPromise OnStart() {
183 protected virtual IPromise OnStart() {
170 return Promise.SUCCESS;
184 return Promise.SUCCESS;
171 }
185 }
172
186
173 protected virtual void Run() {
187 public IPromise Stop() {
188 return InvokeAsync(Commands.Stop, OnStop, StopPending).Then(Dispose);
189 }
190
191 protected virtual IPromise OnStop() {
192 return Promise.SUCCESS;
174 }
193 }
175
194
176 public IPromise Stop() {
195 /// <summary>
177 throw new NotImplementedException();
196 /// Stops the current operation if one exists.
197 /// </summary>
198 /// <param name="current">Current.</param>
199 /// <param name="stop">Stop.</param>
200 protected virtual void StopPending(IPromise current, IDeferred stop) {
201 if (current == null) {
202 stop.Resolve();
203 } else {
204 current.On(stop.Resolve, stop.Reject, stop.CancelOperation);
205 current.Cancel();
206 }
178 }
207 }
179
208
180 public ExecutionState State {
209 public ExecutionState State {
181 get {
210 get {
182 throw new NotImplementedException();
211 return m_stateMachine.State;
183 }
212 }
184 }
213 }
185
214
186 public Exception LastError {
215 public Exception LastError {
187 get {
216 get {
188 throw new NotImplementedException();
217 return m_lastError;
189 }
218 }
190 }
219 }
191
220
192 #endregion
221 #endregion
222
223 #region IDisposable implementation
224
225 public void Dispose() {
226 IPromise pending;
227 lock (m_stateMachine) {
228 if (m_stateMachine.State == ExecutionState.Disposed)
229 return;
230
231 Move(Commands.Dispose);
232
233 GC.SuppressFinalize(this);
234
235 pending = m_pending;
236 m_pending = null;
237 }
238 if (pending != null) {
239 pending.Cancel();
240 pending.Timeout(DisposeTimeout).On(
241 () => Dispose(true, null),
242 err => Dispose(true, err),
243 reason => Dispose(true, new OperationCanceledException("The operation is cancelled", reason))
244 );
245 } else {
246 Dispose(true, m_lastError);
247 }
248 }
249
250 ~RunnableComponent() {
251 Dispose(false, null);
252 }
253
254 #endregion
255
256 protected virtual void Dispose(bool disposing, Exception lastError) {
257
258 }
259
193 }
260 }
194 }
261 }
195
262
@@ -3,11 +3,6 using System;
3 using Implab.Diagnostics;
3 using Implab.Diagnostics;
4 using System.Collections.Generic;
4 using System.Collections.Generic;
5
5
6
7 #if NET_4_5
8 using System.Threading.Tasks;
9 #endif
10
11 namespace Implab {
6 namespace Implab {
12 public static class PromiseExtensions {
7 public static class PromiseExtensions {
13 public static IPromise<T> DispatchToCurrentContext<T>(this IPromise<T> that) {
8 public static IPromise<T> DispatchToCurrentContext<T>(this IPromise<T> that) {
@@ -17,12 +12,12 namespace Implab {
17 return that;
12 return that;
18
13
19 var p = new SyncContextPromise<T>(context);
14 var p = new SyncContextPromise<T>(context);
20 p.On(that.Cancel, PromiseEventType.Cancelled);
15 p.CancellationRequested(that.Cancel);
21
16
22 that.On(
17 that.On(
23 p.Resolve,
18 p.Resolve,
24 p.Reject,
19 p.Reject,
25 p.Cancel
20 p.CancelOperation
26 );
21 );
27 return p;
22 return p;
28 }
23 }
@@ -32,13 +27,12 namespace Implab {
32 Safe.ArgumentNotNull(context, "context");
27 Safe.ArgumentNotNull(context, "context");
33
28
34 var p = new SyncContextPromise<T>(context);
29 var p = new SyncContextPromise<T>(context);
35 p.On(that.Cancel, PromiseEventType.Cancelled);
30 p.CancellationRequested(that.Cancel);
36
37
31
38 that.On(
32 that.On(
39 p.Resolve,
33 p.Resolve,
40 p.Reject,
34 p.Reject,
41 p.Cancel
35 p.CancelOperation
42 );
36 );
43 return p;
37 return p;
44 }
38 }
@@ -77,8 +71,8 namespace Implab {
77 };
71 };
78 }
72 }
79
73
80 static void CancelCallback(object cookie) {
74 static void CancelByTimeoutCallback(object cookie) {
81 ((ICancellable)cookie).Cancel();
75 ((ICancellable)cookie).Cancel(new TimeoutException());
82 }
76 }
83
77
84 /// <summary>
78 /// <summary>
@@ -89,7 +83,7 namespace Implab {
89 /// <typeparam name="TPromise">The 1st type parameter.</typeparam>
83 /// <typeparam name="TPromise">The 1st type parameter.</typeparam>
90 public static TPromise Timeout<TPromise>(this TPromise that, int milliseconds) where TPromise : IPromise {
84 public static TPromise Timeout<TPromise>(this TPromise that, int milliseconds) where TPromise : IPromise {
91 Safe.ArgumentNotNull(that, "that");
85 Safe.ArgumentNotNull(that, "that");
92 var timer = new Timer(CancelCallback, that, milliseconds, -1);
86 var timer = new Timer(CancelByTimeoutCallback, that, milliseconds, -1);
93 that.On(timer.Dispose, PromiseEventType.All);
87 that.On(timer.Dispose, PromiseEventType.All);
94 return that;
88 return that;
95 }
89 }
@@ -180,8 +174,7 namespace Implab {
180
174
181 var d = new ActionTask(success, error, cancel, false);
175 var d = new ActionTask(success, error, cancel, false);
182 that.On(d.Resolve, d.Reject, d.CancelOperation);
176 that.On(d.Resolve, d.Reject, d.CancelOperation);
183 if (success != null)
177 d.CancellationRequested(that.Cancel);
184 d.CancellationRequested(that.Cancel);
185 return d;
178 return d;
186 }
179 }
187
180
@@ -198,8 +191,7 namespace Implab {
198
191
199 var d = new FuncTask<T>(success, error, cancel, false);
192 var d = new FuncTask<T>(success, error, cancel, false);
200 that.On(d.Resolve, d.Reject, d.CancelOperation);
193 that.On(d.Resolve, d.Reject, d.CancelOperation);
201 if (success != null)
194 d.CancellationRequested(that.Cancel);
202 d.CancellationRequested(that.Cancel);
203 return d;
195 return d;
204 }
196 }
205
197
@@ -215,8 +207,7 namespace Implab {
215 Safe.ArgumentNotNull(that, "that");
207 Safe.ArgumentNotNull(that, "that");
216 var d = new FuncTask<T,T2>(success, error, cancel, false);
208 var d = new FuncTask<T,T2>(success, error, cancel, false);
217 that.On(d.Resolve, d.Reject, d.CancelOperation);
209 that.On(d.Resolve, d.Reject, d.CancelOperation);
218 if (success != null)
210 d.CancellationRequested(that.Cancel);
219 d.CancellationRequested(that.Cancel);
220 return d;
211 return d;
221 }
212 }
222
213
@@ -234,8 +225,7 namespace Implab {
234
225
235 var d = new ActionChainTask(success, error, cancel, false);
226 var d = new ActionChainTask(success, error, cancel, false);
236 that.On(d.Resolve, d.Reject, d.CancelOperation);
227 that.On(d.Resolve, d.Reject, d.CancelOperation);
237 if (success != null)
228 d.CancellationRequested(that.Cancel);
238 d.CancellationRequested(that.Cancel);
239 return d;
229 return d;
240 }
230 }
241
231
General Comments 0
You need to be logged in to leave comments. Login now