Changed license to Apache License 2.0

This commit is contained in:
Markus Ewald 2024-06-14 16:42:33 +02:00 committed by cygon
parent 857917aad5
commit 0037b7de46
47 changed files with 5942 additions and 5849 deletions

View file

@ -1,156 +1,155 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2019 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
#if UNITTEST
using System;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Unit test for the dialog view model</summary>
[TestFixture]
public class DialogViewModelTest {
#region class DialogViewModelSubscriber
/// <summary>Subscriber for the events offered by a dialog view model</summary>
private class DialogViewModelSubscriber {
/// <summary>Indicates that the user has accepted the dialog</summary>
public void Confirmed(object sender, EventArgs arguments) {
++this.confirmCallCount;
}
/// <summary>Indicates that the user has cancelled the dialog</summary>
public void Cancelled(object sender, EventArgs arguments) {
++this.cancelCallCount;
}
/// <summary>Indicates that the dialog was simply closed</summary>
public void Submitted(object sender, EventArgs arguments) {
++this.submitCallCount;
}
/// <summary>How many times the Confirmed() method was called</summary>
public int ConfirmCallCount {
get { return this.confirmCallCount; }
}
/// <summary>How many times the Cancelled() method was called</summary>
public int CancelCallCount {
get { return this.cancelCallCount; }
}
/// <summary>How many times the Submitted() method was called</summary>
public int SubmitCallCount {
get { return this.submitCallCount; }
}
/// <summary>How many times the Confirmed() method was called</summary>
private int confirmCallCount;
/// <summary>How many times the Cancelled() method was called</summary>
private int cancelCallCount;
/// <summary>How many times the Submitted() method was called</summary>
private int submitCallCount;
}
#endregion // class DialogViewModelSubscriber
/// <summary>Verifies that the dialog view model has a default constructor</summary>
[Test]
public void HasDefaultConstructor() {
Assert.DoesNotThrow(
delegate() { new DialogViewModel(); }
);
}
/// <summary>
/// Verifies that calling Confirm() on the dialog view model triggers
/// the 'Confirmed' event
/// </summary>
[Test]
public void ConfirmTriggersConfirmedEvent() {
var viewModel = new DialogViewModel();
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
viewModel.Confirm();
Assert.AreEqual(1, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
}
/// <summary>
/// Verifies that calling Cancel() on the dialog view model triggers
/// the 'Cancelled' event
/// </summary>
[Test]
public void CancelTriggersCancelledEvent() {
var viewModel = new DialogViewModel();
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
viewModel.Cancel();
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(1, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
}
/// <summary>
/// Verifies that calling Submitm() on the dialog view model triggers
/// the 'Submitted' event
/// </summary>
[Test]
public void SubmitTriggersSubmittedEvent() {
var viewModel = new DialogViewModel();
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
viewModel.Submit();
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(1, subscriber.SubmitCallCount);
}
/// <summary>Constructs a new subscriber for the dialog view model's events</summary>
/// <param name="viewModel">View model a subscriber will be created for</param>
/// <returns>A subscriber for the events of the specified view model</returns>
private DialogViewModelSubscriber createSubscriber(DialogViewModel viewModel) {
var subscriber = new DialogViewModelSubscriber();
viewModel.Confirmed += subscriber.Confirmed;
viewModel.Canceled += subscriber.Cancelled;
viewModel.Submitted += subscriber.Submitted;
return subscriber;
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
#endif // UNITTEST
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
#if UNITTEST
using System;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Unit test for the dialog view model</summary>
[TestFixture]
public class DialogViewModelTest {
#region class DialogViewModelSubscriber
/// <summary>Subscriber for the events offered by a dialog view model</summary>
private class DialogViewModelSubscriber {
/// <summary>Indicates that the user has accepted the dialog</summary>
public void Confirmed(object sender, EventArgs arguments) {
++this.confirmCallCount;
}
/// <summary>Indicates that the user has cancelled the dialog</summary>
public void Cancelled(object sender, EventArgs arguments) {
++this.cancelCallCount;
}
/// <summary>Indicates that the dialog was simply closed</summary>
public void Submitted(object sender, EventArgs arguments) {
++this.submitCallCount;
}
/// <summary>How many times the Confirmed() method was called</summary>
public int ConfirmCallCount {
get { return this.confirmCallCount; }
}
/// <summary>How many times the Cancelled() method was called</summary>
public int CancelCallCount {
get { return this.cancelCallCount; }
}
/// <summary>How many times the Submitted() method was called</summary>
public int SubmitCallCount {
get { return this.submitCallCount; }
}
/// <summary>How many times the Confirmed() method was called</summary>
private int confirmCallCount;
/// <summary>How many times the Cancelled() method was called</summary>
private int cancelCallCount;
/// <summary>How many times the Submitted() method was called</summary>
private int submitCallCount;
}
#endregion // class DialogViewModelSubscriber
/// <summary>Verifies that the dialog view model has a default constructor</summary>
[Test]
public void HasDefaultConstructor() {
Assert.DoesNotThrow(
delegate() { new DialogViewModel(); }
);
}
/// <summary>
/// Verifies that calling Confirm() on the dialog view model triggers
/// the 'Confirmed' event
/// </summary>
[Test]
public void ConfirmTriggersConfirmedEvent() {
var viewModel = new DialogViewModel();
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
viewModel.Confirm();
Assert.AreEqual(1, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
}
/// <summary>
/// Verifies that calling Cancel() on the dialog view model triggers
/// the 'Cancelled' event
/// </summary>
[Test]
public void CancelTriggersCancelledEvent() {
var viewModel = new DialogViewModel();
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
viewModel.Cancel();
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(1, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
}
/// <summary>
/// Verifies that calling Submitm() on the dialog view model triggers
/// the 'Submitted' event
/// </summary>
[Test]
public void SubmitTriggersSubmittedEvent() {
var viewModel = new DialogViewModel();
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
viewModel.Submit();
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(1, subscriber.SubmitCallCount);
}
/// <summary>Constructs a new subscriber for the dialog view model's events</summary>
/// <param name="viewModel">View model a subscriber will be created for</param>
/// <returns>A subscriber for the events of the specified view model</returns>
private DialogViewModelSubscriber createSubscriber(DialogViewModel viewModel) {
var subscriber = new DialogViewModelSubscriber();
viewModel.Confirmed += subscriber.Confirmed;
viewModel.Canceled += subscriber.Cancelled;
viewModel.Submitted += subscriber.Submitted;
return subscriber;
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
#endif // UNITTEST

View file

@ -1,76 +1,75 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2019 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using Nuclex.Support;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Base class for the view model of dialogs (typically modal ones)</summary>
public class DialogViewModel : Observable {
/// <summary>Indicates that the view should close with a positive result</summary>
/// <remarks>
/// This event typically corresponds to the 'Ok' button in a dialog.
/// </remarks>
public event EventHandler Confirmed;
/// <summary>Indicates that the view should close with a negative result</summary>
/// <remarks>
/// This event typically corresponds to the 'Cancel' button in a dialog.
/// </remarks>
public event EventHandler Canceled;
/// <summary>Indicates that the view should close</summary>
/// <remarks>
/// This closes the view with a neutral result, used when the view doesn't follow
/// an ok/cancel scheme or the result is transmitted in some other way.
/// </remarks>
public event EventHandler Submitted;
/// <summary>
/// Indicates that the dialog should be closed with a positive outcome
/// </summary>
public virtual void Confirm() {
if(Confirmed != null) {
Confirmed(this, EventArgs.Empty);
}
}
/// <summary>
/// Indicates that the dialog should be closed with a negative outcome
/// </summary>
public virtual void Cancel() {
if(Canceled != null) {
Canceled(this, EventArgs.Empty);
}
}
/// <summary>Indicates that the dialog should be closed</summary>
public virtual void Submit() {
if(Submitted != null) {
Submitted(this, EventArgs.Empty);
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using Nuclex.Support;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Base class for the view model of dialogs (typically modal ones)</summary>
public class DialogViewModel : Observable {
/// <summary>Indicates that the view should close with a positive result</summary>
/// <remarks>
/// This event typically corresponds to the 'Ok' button in a dialog.
/// </remarks>
public event EventHandler Confirmed;
/// <summary>Indicates that the view should close with a negative result</summary>
/// <remarks>
/// This event typically corresponds to the 'Cancel' button in a dialog.
/// </remarks>
public event EventHandler Canceled;
/// <summary>Indicates that the view should close</summary>
/// <remarks>
/// This closes the view with a neutral result, used when the view doesn't follow
/// an ok/cancel scheme or the result is transmitted in some other way.
/// </remarks>
public event EventHandler Submitted;
/// <summary>
/// Indicates that the dialog should be closed with a positive outcome
/// </summary>
public virtual void Confirm() {
if(Confirmed != null) {
Confirmed(this, EventArgs.Empty);
}
}
/// <summary>
/// Indicates that the dialog should be closed with a negative outcome
/// </summary>
public virtual void Cancel() {
if(Canceled != null) {
Canceled(this, EventArgs.Empty);
}
}
/// <summary>Indicates that the dialog should be closed</summary>
public virtual void Submit() {
if(Submitted != null) {
Submitted(this, EventArgs.Empty);
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels

View file

@ -1,14 +1,33 @@
using System;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Interface for vew models that can switch between different pages</summary>
public interface IMultiPageViewModel {
/// <summary>Retrieves (and, if needed, creates) the view model for the active page</summary>
/// <returns>A view model for the active page on the multi-page view model</returns>
object GetActivePageViewModel();
}
} // namespace Nuclex.Windows.Forms.ViewModels
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Interface for vew models that can switch between different pages</summary>
public interface IMultiPageViewModel {
/// <summary>Retrieves (and, if needed, creates) the view model for the active page</summary>
/// <returns>A view model for the active page on the multi-page view model</returns>
object GetActivePageViewModel();
}
} // namespace Nuclex.Windows.Forms.ViewModels

View file

@ -1,130 +1,129 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2019 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Collections.Concurrent;
using Nuclex.Support;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Base class for view models that have multiple child view models</summary>
/// <typeparam name="TPageEnumeration">Enum type by which pages can be indicated</typeparam>
public abstract class MultiPageViewModel<TPageEnumeration> :
Observable, IMultiPageViewModel, IDisposable {
/// <summary>Initializes a new multi-page view model</summary>
/// <param name="windowManager">
/// Window manager the view model uses to create child views
/// </param>
/// <param name="cachePageViewModels">
/// Whether child view models will be kept alive and reused
/// </param>
public MultiPageViewModel(IWindowManager windowManager, bool cachePageViewModels = false) {
this.windowManager = windowManager;
if(cachePageViewModels) {
this.cachedViewModels = new ConcurrentDictionary<TPageEnumeration, object>();
}
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public virtual void Dispose() {
if(this.cachedViewModels != null) {
foreach(object cacheViewModel in this.cachedViewModels.Values) {
disposeIfSupported(cacheViewModel);
}
this.activePageViewModel = null;
this.cachedViewModels.Clear();
this.cachedViewModels = null;
} else if(this.activePageViewModel != null) {
disposeIfSupported(this.activePageViewModel);
this.activePageViewModel = null;
}
}
/// <summary>Child page that is currently being displayed by the view model</summary>
public TPageEnumeration ActivePage {
get { return this.activePage; }
set {
if(!this.activePage.Equals(value)) {
this.activePage = value;
if(this.activePageViewModel != null) {
if(this.cachedViewModels == null) {
disposeIfSupported(this.activePageViewModel);
}
this.activePageViewModel = null;
}
OnPropertyChanged(nameof(ActivePage));
}
}
}
/// <summary>Retrieves (and, if needed, creates) the view model for the active page</summary>
/// <returns>A view model for the active page on the multi-page view model</returns>
public object GetActivePageViewModel() {
if(this.cachedViewModels == null) {
if(this.activePageViewModel == null) {
this.activePageViewModel = CreateViewModelForPage(this.activePage);
}
} else if(this.activePageViewModel == null) {
this.activePageViewModel = this.cachedViewModels.GetOrAdd(
this.activePage,
delegate(TPageEnumeration activePage) {
return CreateViewModelForPage(this.activePage);
}
);
}
return this.activePageViewModel;
}
/// <summary>Windowmanager that can create view models and display other views</summary>
protected IWindowManager WindowManager {
get { return this.windowManager; }
}
/// <summary>Creates a view model for the specified page</summary>
/// <param name="page">Page for which a view model will be created</param>
/// <returns>The view model for the specified page</returns>
protected abstract object CreateViewModelForPage(TPageEnumeration page);
/// <summary>Disposes the specified object if it is disposable</summary>
/// <param name="potentiallyDisposable">Object that will be disposed if supported</param>
private static void disposeIfSupported(object potentiallyDisposable) {
var disposable = potentiallyDisposable as IDisposable;
if(disposable != null) {
disposable.Dispose();
}
}
/// <summary>Page that is currently active in the multi-page view model</summary>
private TPageEnumeration activePage;
/// <summary>Window manager that can be used to display other views</summary>
private IWindowManager windowManager;
/// <summary>View model for the active page</summary>
private object activePageViewModel;
/// <summary>Cached page view models, if caching is enabled</summary>
private ConcurrentDictionary<TPageEnumeration, object> cachedViewModels;
}
} // namespace Nuclex.Windows.Forms.ViewModels
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using System.Collections.Concurrent;
using Nuclex.Support;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Base class for view models that have multiple child view models</summary>
/// <typeparam name="TPageEnumeration">Enum type by which pages can be indicated</typeparam>
public abstract class MultiPageViewModel<TPageEnumeration> :
Observable, IMultiPageViewModel, IDisposable {
/// <summary>Initializes a new multi-page view model</summary>
/// <param name="windowManager">
/// Window manager the view model uses to create child views
/// </param>
/// <param name="cachePageViewModels">
/// Whether child view models will be kept alive and reused
/// </param>
public MultiPageViewModel(IWindowManager windowManager, bool cachePageViewModels = false) {
this.windowManager = windowManager;
if(cachePageViewModels) {
this.cachedViewModels = new ConcurrentDictionary<TPageEnumeration, object>();
}
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public virtual void Dispose() {
if(this.cachedViewModels != null) {
foreach(object cacheViewModel in this.cachedViewModels.Values) {
disposeIfSupported(cacheViewModel);
}
this.activePageViewModel = null;
this.cachedViewModels.Clear();
this.cachedViewModels = null;
} else if(this.activePageViewModel != null) {
disposeIfSupported(this.activePageViewModel);
this.activePageViewModel = null;
}
}
/// <summary>Child page that is currently being displayed by the view model</summary>
public TPageEnumeration ActivePage {
get { return this.activePage; }
set {
if(!this.activePage.Equals(value)) {
this.activePage = value;
if(this.activePageViewModel != null) {
if(this.cachedViewModels == null) {
disposeIfSupported(this.activePageViewModel);
}
this.activePageViewModel = null;
}
OnPropertyChanged(nameof(ActivePage));
}
}
}
/// <summary>Retrieves (and, if needed, creates) the view model for the active page</summary>
/// <returns>A view model for the active page on the multi-page view model</returns>
public object GetActivePageViewModel() {
if(this.cachedViewModels == null) {
if(this.activePageViewModel == null) {
this.activePageViewModel = CreateViewModelForPage(this.activePage);
}
} else if(this.activePageViewModel == null) {
this.activePageViewModel = this.cachedViewModels.GetOrAdd(
this.activePage,
delegate(TPageEnumeration activePage) {
return CreateViewModelForPage(this.activePage);
}
);
}
return this.activePageViewModel;
}
/// <summary>Windowmanager that can create view models and display other views</summary>
protected IWindowManager WindowManager {
get { return this.windowManager; }
}
/// <summary>Creates a view model for the specified page</summary>
/// <param name="page">Page for which a view model will be created</param>
/// <returns>The view model for the specified page</returns>
protected abstract object CreateViewModelForPage(TPageEnumeration page);
/// <summary>Disposes the specified object if it is disposable</summary>
/// <param name="potentiallyDisposable">Object that will be disposed if supported</param>
private static void disposeIfSupported(object potentiallyDisposable) {
var disposable = potentiallyDisposable as IDisposable;
if(disposable != null) {
disposable.Dispose();
}
}
/// <summary>Page that is currently active in the multi-page view model</summary>
private TPageEnumeration activePage;
/// <summary>Window manager that can be used to display other views</summary>
private IWindowManager windowManager;
/// <summary>View model for the active page</summary>
private object activePageViewModel;
/// <summary>Cached page view models, if caching is enabled</summary>
private ConcurrentDictionary<TPageEnumeration, object> cachedViewModels;
}
} // namespace Nuclex.Windows.Forms.ViewModels

View file

@ -1,271 +1,270 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2019 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
#if UNITTEST
using System;
using System.ComponentModel;
using System.Threading;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Unit test for the threaded action class</summary>
[TestFixture]
public class ThreadedActionTest {
#region class DummyContext
/// <summary>Synchronization context that does absolutely nothing</summary>
private class DummyContext : ISynchronizeInvoke {
#region class SimpleAsyncResult
/// <summary>Barebones implementation of an asynchronous result</summary>
private class SimpleAsyncResult : IAsyncResult {
/// <summary>Ehether the asynchronous operation is complete</summary>
/// <remarks>
/// Always true because it completes synchronously
/// </remarks>
public bool IsCompleted { get { return true; } }
/// <summary>
/// Wait handle that can be used to wait for the asynchronous operation
/// </summary>
public WaitHandle AsyncWaitHandle {
get { throw new NotImplementedException("Not implemented"); }
}
/// <summary>Custom state that can be used to pass information around</summary>
public object AsyncState {
get { throw new NotImplementedException("Not implemented"); }
}
/// <summary>Whether the asynchronous operation completed synchronously</summary>
public bool CompletedSynchronously { get { return true; } }
/// <summary>The value returned from the asynchronous operation</summary>
public object ReturnedValue;
}
#endregion // class SimpleAsyncResult
/// <summary>Whether the calling thread needs to use Invoke()</summary>
public bool InvokeRequired {
get { return true; }
}
/// <summary>Schedules the specified method for execution in the target thread</summary>
/// <param name="method">Method the target thread will execute when it is idle</param>
/// <param name="arguments">Arguments that will be passed to the method</param>
/// <returns>
/// An asynchronous result handle that can be used to check on the status of
/// the call and wait for its completion
/// </returns>
public IAsyncResult BeginInvoke(Delegate method, object[] arguments) {
var asyncResult = new SimpleAsyncResult();
asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments);
return asyncResult;
}
/// <summary>Waits for the asychronous call to complete</summary>
/// <param name="result">
/// Asynchronous result handle returned by the <see cref="BeginInvoke" /> method
/// </param>
/// <returns>The original result returned by the asychronously called method</returns>
public object EndInvoke(IAsyncResult result) {
return ((SimpleAsyncResult)result).ReturnedValue;
}
/// <summary>
/// Schedules the specified method for execution in the target thread and waits
/// for it to complete
/// </summary>
/// <param name="method">Method that will be executed by the target thread</param>
/// <param name="arguments">Arguments that will be passed to the method</param>
/// <returns>The result returned by the specified method</returns>
public object Invoke(Delegate method, object[] arguments) {
return method.Method.Invoke(method.Target, arguments);
}
}
#endregion // class DummyContext
#region class DummyThreadedAction
/// <summary>Implementation of a threaded action for the unit test</summary>
private class DummyThreadedAction : ThreadedAction {
/// <summary>
/// Initializes a new threaded action, letting the base class figure out the UI thread
/// </summary>
public DummyThreadedAction() : base() {
this.finishedGate = new ManualResetEvent(initialState: false);
}
/// <summary>
/// Initializes a new view model using the specified UI context explicitly
/// </summary>
public DummyThreadedAction(ISynchronizeInvoke uiContext) : base(uiContext) {
this.finishedGate = new ManualResetEvent(initialState: false);
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public override void Dispose() {
base.Dispose();
if(this.finishedGate != null) {
this.finishedGate.Dispose();
this.finishedGate = null;
}
}
/// <summary>Waits until the first background operation is finished</summary>
/// <returns>
/// True if the background operation is finished, false if it is ongoing
/// </returns>
public bool WaitUntilFinished() {
return this.finishedGate.WaitOne(100);
}
/// <summary>Selects the value that will be assigned when the action runs</summary>
/// <param name="valueToAssign">Value the action will assigned when it runs</param>
public void SetValueToAssign(int valueToAssign) {
this.valueToAssign = valueToAssign;
}
/// <summary>Sets up an error the action will fail with when run</summary>
/// <param name="errorToFailWith">Error the action will fail with</param>
public void SetErrorToFailWith(Exception errorToFailWith) {
this.errorToFailWith = errorToFailWith;
}
/// <summary>Last error that was reported by the threaded view model</summary>
public Exception ReportedError {
get { return this.reportedError; }
}
/// <summary>Value that has been assigned from the background thread</summary>
public int AssignedValue {
get { return this.assignedValue; }
}
/// <summary>Executes the threaded action from the background thread</summary>
/// <param name="cancellationToken">Token by which execution can be canceled</param>
protected override void Run(CancellationToken cancellationToken) {
if(this.errorToFailWith != null) {
throw this.errorToFailWith;
}
this.assignedValue = this.valueToAssign;
this.finishedGate.Set();
}
/// <summary>Called when an error occurs in the background thread</summary>
/// <param name="exception">Exception that was thrown in the background thread</param>
protected override void ReportError(Exception exception) {
this.reportedError = exception;
this.finishedGate.Set();
}
/// <summary>Error the action will fail with, if set</summary>
private Exception errorToFailWith;
/// <summary>Value the action will assign to its same-named field</summary>
private int valueToAssign;
/// <summary>Last error that was reported by the threaded view model</summary>
private volatile Exception reportedError;
/// <summary>Triggered when the </summary>
private ManualResetEvent finishedGate;
/// <summary>Value that is assigned through the background thread</summary>
private volatile int assignedValue;
}
#endregion // class DummyThreadedAction
/// <summary>Verifies that the threaded action has a default constructor</summary>
[Test, Explicit]
public void HasDefaultConstructor() {
using(var mainForm = new System.Windows.Forms.Form()) {
mainForm.Show();
try {
mainForm.Visible = false;
using(new DummyThreadedAction()) { }
}
finally {
mainForm.Close();
}
}
}
/// <summary>
/// Verifies that the threaded action can be constructed with a custom UI context
/// </summary>
[Test]
public void HasCustomSychronizationContextConstructor() {
using(new DummyThreadedAction(new DummyContext())) { }
}
/// <summary>Checks that a new threadd action starts out idle and not busy</summary>
[Test]
public void NewInstanceIsNotBusy() {
using(var action = new DummyThreadedAction(new DummyContext())) {
Assert.IsFalse(action.IsBusy);
}
}
/// <summary>
/// Verifies that errors happening in the background processing threads are
/// reported to the main thread
/// </summary>
[Test]
public void ErrorsInBackgroundThreadAreReported() {
using(var action = new DummyThreadedAction(new DummyContext())) {
var testError = new ArgumentException("Mooh");
action.SetErrorToFailWith(testError);
action.Start();
action.WaitUntilFinished();
Assert.AreSame(testError, action.ReportedError);
}
}
/// <summary>
/// Verifies that the background thread actually executes and can do work
/// </summary>
[Test]
public void BackgroundThreadExecutesTasks() {
using(var action = new DummyThreadedAction(new DummyContext())) {
action.SetValueToAssign(42001);
action.Start();
action.WaitUntilFinished();
Assert.AreEqual(42001, action.AssignedValue);
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
#if UNITTEST
using System;
using System.ComponentModel;
using System.Threading;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Unit test for the threaded action class</summary>
[TestFixture]
public class ThreadedActionTest {
#region class DummyContext
/// <summary>Synchronization context that does absolutely nothing</summary>
private class DummyContext : ISynchronizeInvoke {
#region class SimpleAsyncResult
/// <summary>Barebones implementation of an asynchronous result</summary>
private class SimpleAsyncResult : IAsyncResult {
/// <summary>Ehether the asynchronous operation is complete</summary>
/// <remarks>
/// Always true because it completes synchronously
/// </remarks>
public bool IsCompleted { get { return true; } }
/// <summary>
/// Wait handle that can be used to wait for the asynchronous operation
/// </summary>
public WaitHandle AsyncWaitHandle {
get { throw new NotImplementedException("Not implemented"); }
}
/// <summary>Custom state that can be used to pass information around</summary>
public object AsyncState {
get { throw new NotImplementedException("Not implemented"); }
}
/// <summary>Whether the asynchronous operation completed synchronously</summary>
public bool CompletedSynchronously { get { return true; } }
/// <summary>The value returned from the asynchronous operation</summary>
public object ReturnedValue;
}
#endregion // class SimpleAsyncResult
/// <summary>Whether the calling thread needs to use Invoke()</summary>
public bool InvokeRequired {
get { return true; }
}
/// <summary>Schedules the specified method for execution in the target thread</summary>
/// <param name="method">Method the target thread will execute when it is idle</param>
/// <param name="arguments">Arguments that will be passed to the method</param>
/// <returns>
/// An asynchronous result handle that can be used to check on the status of
/// the call and wait for its completion
/// </returns>
public IAsyncResult BeginInvoke(Delegate method, object[] arguments) {
var asyncResult = new SimpleAsyncResult();
asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments);
return asyncResult;
}
/// <summary>Waits for the asychronous call to complete</summary>
/// <param name="result">
/// Asynchronous result handle returned by the <see cref="BeginInvoke" /> method
/// </param>
/// <returns>The original result returned by the asychronously called method</returns>
public object EndInvoke(IAsyncResult result) {
return ((SimpleAsyncResult)result).ReturnedValue;
}
/// <summary>
/// Schedules the specified method for execution in the target thread and waits
/// for it to complete
/// </summary>
/// <param name="method">Method that will be executed by the target thread</param>
/// <param name="arguments">Arguments that will be passed to the method</param>
/// <returns>The result returned by the specified method</returns>
public object Invoke(Delegate method, object[] arguments) {
return method.Method.Invoke(method.Target, arguments);
}
}
#endregion // class DummyContext
#region class DummyThreadedAction
/// <summary>Implementation of a threaded action for the unit test</summary>
private class DummyThreadedAction : ThreadedAction {
/// <summary>
/// Initializes a new threaded action, letting the base class figure out the UI thread
/// </summary>
public DummyThreadedAction() : base() {
this.finishedGate = new ManualResetEvent(initialState: false);
}
/// <summary>
/// Initializes a new view model using the specified UI context explicitly
/// </summary>
public DummyThreadedAction(ISynchronizeInvoke uiContext) : base(uiContext) {
this.finishedGate = new ManualResetEvent(initialState: false);
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public override void Dispose() {
base.Dispose();
if(this.finishedGate != null) {
this.finishedGate.Dispose();
this.finishedGate = null;
}
}
/// <summary>Waits until the first background operation is finished</summary>
/// <returns>
/// True if the background operation is finished, false if it is ongoing
/// </returns>
public bool WaitUntilFinished() {
return this.finishedGate.WaitOne(100);
}
/// <summary>Selects the value that will be assigned when the action runs</summary>
/// <param name="valueToAssign">Value the action will assigned when it runs</param>
public void SetValueToAssign(int valueToAssign) {
this.valueToAssign = valueToAssign;
}
/// <summary>Sets up an error the action will fail with when run</summary>
/// <param name="errorToFailWith">Error the action will fail with</param>
public void SetErrorToFailWith(Exception errorToFailWith) {
this.errorToFailWith = errorToFailWith;
}
/// <summary>Last error that was reported by the threaded view model</summary>
public Exception ReportedError {
get { return this.reportedError; }
}
/// <summary>Value that has been assigned from the background thread</summary>
public int AssignedValue {
get { return this.assignedValue; }
}
/// <summary>Executes the threaded action from the background thread</summary>
/// <param name="cancellationToken">Token by which execution can be canceled</param>
protected override void Run(CancellationToken cancellationToken) {
if(this.errorToFailWith != null) {
throw this.errorToFailWith;
}
this.assignedValue = this.valueToAssign;
this.finishedGate.Set();
}
/// <summary>Called when an error occurs in the background thread</summary>
/// <param name="exception">Exception that was thrown in the background thread</param>
protected override void ReportError(Exception exception) {
this.reportedError = exception;
this.finishedGate.Set();
}
/// <summary>Error the action will fail with, if set</summary>
private Exception errorToFailWith;
/// <summary>Value the action will assign to its same-named field</summary>
private int valueToAssign;
/// <summary>Last error that was reported by the threaded view model</summary>
private volatile Exception reportedError;
/// <summary>Triggered when the </summary>
private ManualResetEvent finishedGate;
/// <summary>Value that is assigned through the background thread</summary>
private volatile int assignedValue;
}
#endregion // class DummyThreadedAction
/// <summary>Verifies that the threaded action has a default constructor</summary>
[Test, Explicit]
public void HasDefaultConstructor() {
using(var mainForm = new System.Windows.Forms.Form()) {
mainForm.Show();
try {
mainForm.Visible = false;
using(new DummyThreadedAction()) { }
}
finally {
mainForm.Close();
}
}
}
/// <summary>
/// Verifies that the threaded action can be constructed with a custom UI context
/// </summary>
[Test]
public void HasCustomSychronizationContextConstructor() {
using(new DummyThreadedAction(new DummyContext())) { }
}
/// <summary>Checks that a new threadd action starts out idle and not busy</summary>
[Test]
public void NewInstanceIsNotBusy() {
using(var action = new DummyThreadedAction(new DummyContext())) {
Assert.IsFalse(action.IsBusy);
}
}
/// <summary>
/// Verifies that errors happening in the background processing threads are
/// reported to the main thread
/// </summary>
[Test]
public void ErrorsInBackgroundThreadAreReported() {
using(var action = new DummyThreadedAction(new DummyContext())) {
var testError = new ArgumentException("Mooh");
action.SetErrorToFailWith(testError);
action.Start();
action.WaitUntilFinished();
Assert.AreSame(testError, action.ReportedError);
}
}
/// <summary>
/// Verifies that the background thread actually executes and can do work
/// </summary>
[Test]
public void BackgroundThreadExecutesTasks() {
using(var action = new DummyThreadedAction(new DummyContext())) {
action.SetValueToAssign(42001);
action.Start();
action.WaitUntilFinished();
Assert.AreEqual(42001, action.AssignedValue);
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
#endif // UNITTEST

View file

@ -1,398 +1,397 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2019 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
using Nuclex.Support;
using Nuclex.Support.Threading;
// Possible problem:
//
// After Run() is called, the action may not actually run if
// it is using another thread runner and that one is cancelled.
//
// Thus, a second call to Run() has to schedule the action again,
// even if it might already be scheduled, but should also not execute
// the action a second time if is was indeed still scheduled.
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Encapsulates an action that can run in a thread</summary>
/// <remarks>
/// <para>
/// Sometimes a view model wants to allow multiple actions to take place
/// at the same time. Think multiple panels on the view require updating
/// from a web service - you can make both requests at the same time
/// instead of sequentially.
/// </para>
/// <para>
/// This class is also of use for things that need to be done sequentially
/// by sharing the thread runner of the threaded view model. That way,
/// you still have cancellable actions you can run at will and they
/// automatically queue themselves to be executed one after another.
/// </para>
/// </remarks>
public abstract class ThreadedAction : Observable, IDisposable {
#region class ThreadedActionThreadRunner
/// <summary>Thread runner for the threaded action</summary>
private class ThreadedActionThreadRunner : ThreadRunner {
/// <summary>Initializes a new thread runner for the threaded view model</summary>
public ThreadedActionThreadRunner(ThreadedAction viewModel) {
this.threadedAction = viewModel;
}
/// <summary>Reports an error</summary>
/// <param name="exception">Error that will be reported</param>
protected override void ReportError(Exception exception) {
this.threadedAction.reportErrorFromThread(exception);
}
/// <summary>Called when the status of the busy flag changes</summary>
protected override void BusyChanged() {
// Narf. Can't use this.
}
/// <summary>View model the thread runner belongs to</summary>
private ThreadedAction threadedAction;
}
#endregion // class ThreadedActionThreadRunner
/// <summary>Initializes all common fields of the instance</summary>
private ThreadedAction() {
this.callRunIfNotCancelledDelegate = new Action<CancellationTokenSource>(
callThreadedExecuteIfNotCancelled
);
this.reportErrorDelegate = new Action<Exception>(ReportError);
}
/// <summary>Initializes a threaded action that uses its own thread runner</summary>
public ThreadedAction(ISynchronizeInvoke uiContext = null) : this() {
if(uiContext == null) {
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
if(this.uiContext == null) {
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
}
} else {
this.uiContext = uiContext;
}
this.ownThreadRunner = new ThreadedActionThreadRunner(this);
}
/// <summary>
/// Initializes a threaded action that uses the view model's thread runner
/// </summary>
/// <param name="viewModel">View model whose thread runner will be used</param>
/// <param name="uiContext">
/// UI dispatcher that can be used to run callbacks in the UI thread
/// </param>
public ThreadedAction(
ThreadedViewModel viewModel, ISynchronizeInvoke uiContext = null
) : this() {
if(uiContext == null) {
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
if(this.uiContext == null) {
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
}
} else {
this.uiContext = uiContext;
}
this.externalThreadRunner = viewModel.ThreadRunner;
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public virtual void Dispose() {
if(this.isBusy) {
Cancel();
}
if(this.ownThreadRunner != null) {
this.ownThreadRunner.Dispose();
this.ownThreadRunner = null;
}
if(this.currentCancellationTokenSource != null) {
this.currentCancellationTokenSource.Dispose();
this.currentCancellationTokenSource = null;
}
}
/// <summary>Whether the view model is currently busy executing a task</summary>
public bool IsBusy {
get { return this.isBusy; }
private set {
if(value != this.isBusy) {
this.isBusy = value;
OnPropertyChanged(nameof(IsBusy));
}
}
}
/// <summary>Cancels the running background task, if any</summary>
public void Cancel() {
lock(this.runningTaskSyncRoot) {
// If the background task is not running, do nothing. This also allows
// us to avoid needless recreation of the same cancellation token source.
if(!this.isBusy) {
return;
}
// If a task is currently running, cancel it
if(this.isRunning) {
if(this.currentCancellationTokenSource != null) {
this.currentCancellationTokenSource.Cancel();
this.currentCancellationTokenSource = null;
}
}
// If the task was scheduled to be repeated, we also have to mark
// the upcoming cancellation token source as canceled because the scheduled
// run will still be happening (it will just cancel out immediately).
if(this.nextCancellationTokenSource != null) {
this.nextCancellationTokenSource.Cancel();
this.nextCancellationTokenSource = null;
}
this.isScheduledAgain = false;
// If the task was not running, we can clear the busy state because it
// is not going to reach the running state.
if(!this.isRunning) {
this.isBusy = false;
}
}
}
/// <summary>
/// Starts the task, cancelling the running task before doing so
/// </summary>
public void Restart() {
bool reportBusyChange = false;
lock(this.runningTaskSyncRoot) {
// If we're already in the execution phase, schedule another execution right
// after this one is finished (because now, data might have changed after
// execution has finished).
if(this.isRunning) {
//System.Diagnostics.Debug.WriteLine("Restart() - interrupting execution");
if(this.currentCancellationTokenSource != null) {
this.currentCancellationTokenSource.Cancel();
}
this.currentCancellationTokenSource = this.nextCancellationTokenSource;
this.nextCancellationTokenSource = null;
this.isScheduledAgain = false;
}
// If there's no cancellation token source, create one. If an execution
// was already scheduled and the cancellation token source is still valid,
// then reuse that in order to be able to cancel all scheduled executions.
if(this.currentCancellationTokenSource == null) {
//System.Diagnostics.Debug.WriteLine("Restart() - creating new cancellation token");
this.currentCancellationTokenSource = new CancellationTokenSource();
}
// Schedule another execution of the action
scheduleExecution();
reportBusyChange = (this.isBusy == false);
this.isBusy = true;
}
if(reportBusyChange) {
OnPropertyChanged(nameof(IsBusy));
}
}
/// <summary>Starts the task</summary>
public void Start() {
bool reportBusyChange = false;
lock(this.runningTaskSyncRoot) {
// If we're already in the execution phase, schedule another execution right
// after this one is finished (because now, data might have changed after
// execution has finished).
if(this.isRunning) {
// If we already created a new cancellation token source, keep it,
// otherwise create a new one for the next execution
if(!this.isScheduledAgain) {
this.nextCancellationTokenSource = new CancellationTokenSource();
this.isScheduledAgain = true;
}
} else {
// If there's no cancellation token source, create one. If an execution
// was already scheduled and the cancellation token source is still valid,
// then reuse that in order to be able to cancel all scheduled executions.
if(this.currentCancellationTokenSource == null) {
this.currentCancellationTokenSource = new CancellationTokenSource();
}
// Schedule another execution of the action
scheduleExecution();
}
reportBusyChange = (this.isBusy == false);
this.isBusy = true;
}
if(reportBusyChange) {
OnPropertyChanged(nameof(IsBusy));
}
}
/// <summary>Reports an error</summary>
/// <param name="exception">Error that will be reported</param>
protected abstract void ReportError(Exception exception);
/// <summary>Executes the threaded action from the background thread</summary>
/// <param name="cancellationToken">Token by which execution can be canceled</param>
protected abstract void Run(CancellationToken cancellationToken);
/// <summary>
/// Calls the Run() method from the background thread and manages the flags
/// </summary>
/// <param name="cancellationTokenSource"></param>
private void callThreadedExecuteIfNotCancelled(
CancellationTokenSource cancellationTokenSource
) {
lock(this) {
if(cancellationTokenSource.Token.IsCancellationRequested) {
return;
}
this.isRunning = true;
}
try {
Run(cancellationTokenSource.Token);
}
finally {
bool reportBusyChange = false;
lock(this) {
this.isRunning = false;
// Cancel the current cancellation token because this execution may have
// been scheduled multiple times (there's no way for the Run() method to
// know if the currently scheduled execution was cancelled, so it is forced
// to reschedule on each call - accepting redundant schedules).
cancellationTokenSource.Cancel();
// Pick the next cancellation token source. Normally it is null, but this
// is more elegant because we can avoid an while if statement this way :)
this.currentCancellationTokenSource = nextCancellationTokenSource;
this.nextCancellationTokenSource = null;
// If Start() was called while we were executing, another execution is required
// (because the data may have changed during the call to Start()).
if(this.isScheduledAgain) {
this.isScheduledAgain = false;
scheduleExecution();
} else { // We're idle now
reportBusyChange = (this.isBusy == true);
this.isBusy = false;
}
}
if(reportBusyChange) {
OnPropertyChanged(nameof(IsBusy));
}
}
}
/// <summary>Schedules one execution of the action</summary>
private void scheduleExecution() {
//System.Diagnostics.Debug.WriteLine("Scheduling execution");
ThreadRunner runner = this.externalThreadRunner;
if(runner != null) {
runner.RunInBackground(
this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource
);
}
runner = this.ownThreadRunner;
if(runner != null) {
runner.RunInBackground(
this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource
);
}
}
/// <summary>Reports an error that occurred in the runner's background thread</summary>
/// <param name="exception">Exception that the thread has encountered</param>
private void reportErrorFromThread(Exception exception) {
this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception });
}
/// <summary>Sets the UI context that will be used by the threaded action</summary>
/// <param name="uiContext">The UI context the threaded action will use</param>
private void updateUiContext(ISynchronizeInvoke uiContext) {
this.uiContext = uiContext;
}
/// <summary>Synchronization context of the thread in which the view runs</summary>
private ISynchronizeInvoke uiContext;
/// <summary>Delegate for the ReportError() method</summary>
private Action<Exception> reportErrorDelegate;
/// <summary>Delegate for the callThreadedExecuteIfNotCancelled() method</summary>
private Action<CancellationTokenSource> callRunIfNotCancelledDelegate;
/// <summary>Thread runner on which the action can run its background task</summary>
private ThreadedActionThreadRunner ownThreadRunner;
/// <summary>
/// External thread runner on which the action runs its background task if assigned
/// </summary>
private ThreadRunner externalThreadRunner;
/// <summary>Synchronization root for the threaded execute method</summary>
private object runningTaskSyncRoot = new object();
/// <summary>Used to cancel the currently running task</summary>
private CancellationTokenSource currentCancellationTokenSource;
/// <summary>Used to cancel the upcoming task if a re-run was scheduled</summary>
private CancellationTokenSource nextCancellationTokenSource;
/// <summary>Whether the background task is running or waiting to run</summary>
private volatile bool isBusy;
/// <summary>Whether execution is taking place right now</summary>
/// <remarks>
/// If this flag is set and the Start() method is called, another run needs to
/// be scheduled.
/// </remarks>
private bool isRunning;
/// <summary>Whether run was called while the action was already running</summary>
private bool isScheduledAgain;
}
} // namespace Nuclex.Windows.Forms.ViewModels
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
using Nuclex.Support;
using Nuclex.Support.Threading;
// Possible problem:
//
// After Run() is called, the action may not actually run if
// it is using another thread runner and that one is cancelled.
//
// Thus, a second call to Run() has to schedule the action again,
// even if it might already be scheduled, but should also not execute
// the action a second time if is was indeed still scheduled.
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Encapsulates an action that can run in a thread</summary>
/// <remarks>
/// <para>
/// Sometimes a view model wants to allow multiple actions to take place
/// at the same time. Think multiple panels on the view require updating
/// from a web service - you can make both requests at the same time
/// instead of sequentially.
/// </para>
/// <para>
/// This class is also of use for things that need to be done sequentially
/// by sharing the thread runner of the threaded view model. That way,
/// you still have cancellable actions you can run at will and they
/// automatically queue themselves to be executed one after another.
/// </para>
/// </remarks>
public abstract class ThreadedAction : Observable, IDisposable {
#region class ThreadedActionThreadRunner
/// <summary>Thread runner for the threaded action</summary>
private class ThreadedActionThreadRunner : ThreadRunner {
/// <summary>Initializes a new thread runner for the threaded view model</summary>
public ThreadedActionThreadRunner(ThreadedAction viewModel) {
this.threadedAction = viewModel;
}
/// <summary>Reports an error</summary>
/// <param name="exception">Error that will be reported</param>
protected override void ReportError(Exception exception) {
this.threadedAction.reportErrorFromThread(exception);
}
/// <summary>Called when the status of the busy flag changes</summary>
protected override void BusyChanged() {
// Narf. Can't use this.
}
/// <summary>View model the thread runner belongs to</summary>
private ThreadedAction threadedAction;
}
#endregion // class ThreadedActionThreadRunner
/// <summary>Initializes all common fields of the instance</summary>
private ThreadedAction() {
this.callRunIfNotCancelledDelegate = new Action<CancellationTokenSource>(
callThreadedExecuteIfNotCancelled
);
this.reportErrorDelegate = new Action<Exception>(ReportError);
}
/// <summary>Initializes a threaded action that uses its own thread runner</summary>
public ThreadedAction(ISynchronizeInvoke uiContext = null) : this() {
if(uiContext == null) {
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
if(this.uiContext == null) {
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
}
} else {
this.uiContext = uiContext;
}
this.ownThreadRunner = new ThreadedActionThreadRunner(this);
}
/// <summary>
/// Initializes a threaded action that uses the view model's thread runner
/// </summary>
/// <param name="viewModel">View model whose thread runner will be used</param>
/// <param name="uiContext">
/// UI dispatcher that can be used to run callbacks in the UI thread
/// </param>
public ThreadedAction(
ThreadedViewModel viewModel, ISynchronizeInvoke uiContext = null
) : this() {
if(uiContext == null) {
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
if(this.uiContext == null) {
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
}
} else {
this.uiContext = uiContext;
}
this.externalThreadRunner = viewModel.ThreadRunner;
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public virtual void Dispose() {
if(this.isBusy) {
Cancel();
}
if(this.ownThreadRunner != null) {
this.ownThreadRunner.Dispose();
this.ownThreadRunner = null;
}
if(this.currentCancellationTokenSource != null) {
this.currentCancellationTokenSource.Dispose();
this.currentCancellationTokenSource = null;
}
}
/// <summary>Whether the view model is currently busy executing a task</summary>
public bool IsBusy {
get { return this.isBusy; }
private set {
if(value != this.isBusy) {
this.isBusy = value;
OnPropertyChanged(nameof(IsBusy));
}
}
}
/// <summary>Cancels the running background task, if any</summary>
public void Cancel() {
lock(this.runningTaskSyncRoot) {
// If the background task is not running, do nothing. This also allows
// us to avoid needless recreation of the same cancellation token source.
if(!this.isBusy) {
return;
}
// If a task is currently running, cancel it
if(this.isRunning) {
if(this.currentCancellationTokenSource != null) {
this.currentCancellationTokenSource.Cancel();
this.currentCancellationTokenSource = null;
}
}
// If the task was scheduled to be repeated, we also have to mark
// the upcoming cancellation token source as canceled because the scheduled
// run will still be happening (it will just cancel out immediately).
if(this.nextCancellationTokenSource != null) {
this.nextCancellationTokenSource.Cancel();
this.nextCancellationTokenSource = null;
}
this.isScheduledAgain = false;
// If the task was not running, we can clear the busy state because it
// is not going to reach the running state.
if(!this.isRunning) {
this.isBusy = false;
}
}
}
/// <summary>
/// Starts the task, cancelling the running task before doing so
/// </summary>
public void Restart() {
bool reportBusyChange = false;
lock(this.runningTaskSyncRoot) {
// If we're already in the execution phase, schedule another execution right
// after this one is finished (because now, data might have changed after
// execution has finished).
if(this.isRunning) {
//System.Diagnostics.Debug.WriteLine("Restart() - interrupting execution");
if(this.currentCancellationTokenSource != null) {
this.currentCancellationTokenSource.Cancel();
}
this.currentCancellationTokenSource = this.nextCancellationTokenSource;
this.nextCancellationTokenSource = null;
this.isScheduledAgain = false;
}
// If there's no cancellation token source, create one. If an execution
// was already scheduled and the cancellation token source is still valid,
// then reuse that in order to be able to cancel all scheduled executions.
if(this.currentCancellationTokenSource == null) {
//System.Diagnostics.Debug.WriteLine("Restart() - creating new cancellation token");
this.currentCancellationTokenSource = new CancellationTokenSource();
}
// Schedule another execution of the action
scheduleExecution();
reportBusyChange = (this.isBusy == false);
this.isBusy = true;
}
if(reportBusyChange) {
OnPropertyChanged(nameof(IsBusy));
}
}
/// <summary>Starts the task</summary>
public void Start() {
bool reportBusyChange = false;
lock(this.runningTaskSyncRoot) {
// If we're already in the execution phase, schedule another execution right
// after this one is finished (because now, data might have changed after
// execution has finished).
if(this.isRunning) {
// If we already created a new cancellation token source, keep it,
// otherwise create a new one for the next execution
if(!this.isScheduledAgain) {
this.nextCancellationTokenSource = new CancellationTokenSource();
this.isScheduledAgain = true;
}
} else {
// If there's no cancellation token source, create one. If an execution
// was already scheduled and the cancellation token source is still valid,
// then reuse that in order to be able to cancel all scheduled executions.
if(this.currentCancellationTokenSource == null) {
this.currentCancellationTokenSource = new CancellationTokenSource();
}
// Schedule another execution of the action
scheduleExecution();
}
reportBusyChange = (this.isBusy == false);
this.isBusy = true;
}
if(reportBusyChange) {
OnPropertyChanged(nameof(IsBusy));
}
}
/// <summary>Reports an error</summary>
/// <param name="exception">Error that will be reported</param>
protected abstract void ReportError(Exception exception);
/// <summary>Executes the threaded action from the background thread</summary>
/// <param name="cancellationToken">Token by which execution can be canceled</param>
protected abstract void Run(CancellationToken cancellationToken);
/// <summary>
/// Calls the Run() method from the background thread and manages the flags
/// </summary>
/// <param name="cancellationTokenSource"></param>
private void callThreadedExecuteIfNotCancelled(
CancellationTokenSource cancellationTokenSource
) {
lock(this) {
if(cancellationTokenSource.Token.IsCancellationRequested) {
return;
}
this.isRunning = true;
}
try {
Run(cancellationTokenSource.Token);
}
finally {
bool reportBusyChange = false;
lock(this) {
this.isRunning = false;
// Cancel the current cancellation token because this execution may have
// been scheduled multiple times (there's no way for the Run() method to
// know if the currently scheduled execution was cancelled, so it is forced
// to reschedule on each call - accepting redundant schedules).
cancellationTokenSource.Cancel();
// Pick the next cancellation token source. Normally it is null, but this
// is more elegant because we can avoid an while if statement this way :)
this.currentCancellationTokenSource = nextCancellationTokenSource;
this.nextCancellationTokenSource = null;
// If Start() was called while we were executing, another execution is required
// (because the data may have changed during the call to Start()).
if(this.isScheduledAgain) {
this.isScheduledAgain = false;
scheduleExecution();
} else { // We're idle now
reportBusyChange = (this.isBusy == true);
this.isBusy = false;
}
}
if(reportBusyChange) {
OnPropertyChanged(nameof(IsBusy));
}
}
}
/// <summary>Schedules one execution of the action</summary>
private void scheduleExecution() {
//System.Diagnostics.Debug.WriteLine("Scheduling execution");
ThreadRunner runner = this.externalThreadRunner;
if(runner != null) {
runner.RunInBackground(
this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource
);
}
runner = this.ownThreadRunner;
if(runner != null) {
runner.RunInBackground(
this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource
);
}
}
/// <summary>Reports an error that occurred in the runner's background thread</summary>
/// <param name="exception">Exception that the thread has encountered</param>
private void reportErrorFromThread(Exception exception) {
this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception });
}
/// <summary>Sets the UI context that will be used by the threaded action</summary>
/// <param name="uiContext">The UI context the threaded action will use</param>
private void updateUiContext(ISynchronizeInvoke uiContext) {
this.uiContext = uiContext;
}
/// <summary>Synchronization context of the thread in which the view runs</summary>
private ISynchronizeInvoke uiContext;
/// <summary>Delegate for the ReportError() method</summary>
private Action<Exception> reportErrorDelegate;
/// <summary>Delegate for the callThreadedExecuteIfNotCancelled() method</summary>
private Action<CancellationTokenSource> callRunIfNotCancelledDelegate;
/// <summary>Thread runner on which the action can run its background task</summary>
private ThreadedActionThreadRunner ownThreadRunner;
/// <summary>
/// External thread runner on which the action runs its background task if assigned
/// </summary>
private ThreadRunner externalThreadRunner;
/// <summary>Synchronization root for the threaded execute method</summary>
private object runningTaskSyncRoot = new object();
/// <summary>Used to cancel the currently running task</summary>
private CancellationTokenSource currentCancellationTokenSource;
/// <summary>Used to cancel the upcoming task if a re-run was scheduled</summary>
private CancellationTokenSource nextCancellationTokenSource;
/// <summary>Whether the background task is running or waiting to run</summary>
private volatile bool isBusy;
/// <summary>Whether execution is taking place right now</summary>
/// <remarks>
/// If this flag is set and the Start() method is called, another run needs to
/// be scheduled.
/// </remarks>
private bool isRunning;
/// <summary>Whether run was called while the action was already running</summary>
private bool isScheduledAgain;
}
} // namespace Nuclex.Windows.Forms.ViewModels

View file

@ -1,174 +1,173 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2019 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
#if UNITTEST
using System;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Unit test for the threaded dialog view model</summary>
[TestFixture]
public class ThreadedDialogViewModelTest {
#region class DialogViewModelSubscriber
/// <summary>Subscriber for the events offered by a dialog view model</summary>
private class DialogViewModelSubscriber {
/// <summary>Indicates that the user has accepted the dialog</summary>
public void Confirmed(object sender, EventArgs arguments) {
++this.confirmCallCount;
}
/// <summary>Indicates that the user has cancelled the dialog</summary>
public void Cancelled(object sender, EventArgs arguments) {
++this.cancelCallCount;
}
/// <summary>Indicates that the dialog was simply closed</summary>
public void Submitted(object sender, EventArgs arguments) {
++this.submitCallCount;
}
/// <summary>How many times the Confirmed() method was called</summary>
public int ConfirmCallCount {
get { return this.confirmCallCount; }
}
/// <summary>How many times the Cancelled() method was called</summary>
public int CancelCallCount {
get { return this.cancelCallCount; }
}
/// <summary>How many times the Submitted() method was called</summary>
public int SubmitCallCount {
get { return this.submitCallCount; }
}
/// <summary>How many times the Confirmed() method was called</summary>
private int confirmCallCount;
/// <summary>How many times the Cancelled() method was called</summary>
private int cancelCallCount;
/// <summary>How many times the Submitted() method was called</summary>
private int submitCallCount;
}
#endregion // class DialogViewModelSubscriber
#region class TestViewModel
private class TestViewModel : ThreadedDialogViewModel {
public Exception ReportedError {
get { return this.reportedError; }
}
protected override void ReportError(Exception exception) {
this.reportedError = exception;
}
private Exception reportedError;
}
#endregion // class TestViewModel
/// <summary>Verifies that the dialog view model has a default constructor</summary>
[Test]
public void HasDefaultConstructor() {
Assert.DoesNotThrow(
delegate() { new TestViewModel(); }
);
}
/// <summary>
/// Verifies that calling Confirm() on the dialog view model triggers
/// the 'Confirmed' event
/// </summary>
[Test]
public void ConfirmTriggersConfirmedEvent() {
var viewModel = new TestViewModel();
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
viewModel.Confirm();
Assert.AreEqual(1, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
}
/// <summary>
/// Verifies that calling Cancel() on the dialog view model triggers
/// the 'Cancelled' event
/// </summary>
[Test]
public void CancelTriggersCancelledEvent() {
var viewModel = new TestViewModel();
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
viewModel.Cancel();
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(1, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
}
/// <summary>
/// Verifies that calling Submitm() on the dialog view model triggers
/// the 'Submitted' event
/// </summary>
[Test]
public void SubmitTriggersSubmittedEvent() {
var viewModel = new TestViewModel();
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
viewModel.Submit();
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(1, subscriber.SubmitCallCount);
}
/// <summary>Constructs a new subscriber for the dialog view model's events</summary>
/// <param name="viewModel">View model a subscriber will be created for</param>
/// <returns>A subscriber for the events of the specified view model</returns>
private DialogViewModelSubscriber createSubscriber(ThreadedDialogViewModel viewModel) {
var subscriber = new DialogViewModelSubscriber();
viewModel.Confirmed += subscriber.Confirmed;
viewModel.Canceled += subscriber.Cancelled;
viewModel.Submitted += subscriber.Submitted;
return subscriber;
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
#endif // UNITTEST
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
#if UNITTEST
using System;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Unit test for the threaded dialog view model</summary>
[TestFixture]
public class ThreadedDialogViewModelTest {
#region class DialogViewModelSubscriber
/// <summary>Subscriber for the events offered by a dialog view model</summary>
private class DialogViewModelSubscriber {
/// <summary>Indicates that the user has accepted the dialog</summary>
public void Confirmed(object sender, EventArgs arguments) {
++this.confirmCallCount;
}
/// <summary>Indicates that the user has cancelled the dialog</summary>
public void Cancelled(object sender, EventArgs arguments) {
++this.cancelCallCount;
}
/// <summary>Indicates that the dialog was simply closed</summary>
public void Submitted(object sender, EventArgs arguments) {
++this.submitCallCount;
}
/// <summary>How many times the Confirmed() method was called</summary>
public int ConfirmCallCount {
get { return this.confirmCallCount; }
}
/// <summary>How many times the Cancelled() method was called</summary>
public int CancelCallCount {
get { return this.cancelCallCount; }
}
/// <summary>How many times the Submitted() method was called</summary>
public int SubmitCallCount {
get { return this.submitCallCount; }
}
/// <summary>How many times the Confirmed() method was called</summary>
private int confirmCallCount;
/// <summary>How many times the Cancelled() method was called</summary>
private int cancelCallCount;
/// <summary>How many times the Submitted() method was called</summary>
private int submitCallCount;
}
#endregion // class DialogViewModelSubscriber
#region class TestViewModel
private class TestViewModel : ThreadedDialogViewModel {
public Exception ReportedError {
get { return this.reportedError; }
}
protected override void ReportError(Exception exception) {
this.reportedError = exception;
}
private Exception reportedError;
}
#endregion // class TestViewModel
/// <summary>Verifies that the dialog view model has a default constructor</summary>
[Test]
public void HasDefaultConstructor() {
Assert.DoesNotThrow(
delegate() { new TestViewModel(); }
);
}
/// <summary>
/// Verifies that calling Confirm() on the dialog view model triggers
/// the 'Confirmed' event
/// </summary>
[Test]
public void ConfirmTriggersConfirmedEvent() {
var viewModel = new TestViewModel();
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
viewModel.Confirm();
Assert.AreEqual(1, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
}
/// <summary>
/// Verifies that calling Cancel() on the dialog view model triggers
/// the 'Cancelled' event
/// </summary>
[Test]
public void CancelTriggersCancelledEvent() {
var viewModel = new TestViewModel();
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
viewModel.Cancel();
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(1, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
}
/// <summary>
/// Verifies that calling Submitm() on the dialog view model triggers
/// the 'Submitted' event
/// </summary>
[Test]
public void SubmitTriggersSubmittedEvent() {
var viewModel = new TestViewModel();
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount);
viewModel.Submit();
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(1, subscriber.SubmitCallCount);
}
/// <summary>Constructs a new subscriber for the dialog view model's events</summary>
/// <param name="viewModel">View model a subscriber will be created for</param>
/// <returns>A subscriber for the events of the specified view model</returns>
private DialogViewModelSubscriber createSubscriber(ThreadedDialogViewModel viewModel) {
var subscriber = new DialogViewModelSubscriber();
viewModel.Confirmed += subscriber.Confirmed;
viewModel.Canceled += subscriber.Cancelled;
viewModel.Submitted += subscriber.Submitted;
return subscriber;
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
#endif // UNITTEST

View file

@ -1,74 +1,73 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2019 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>View model for a dialog that can execute tasks in a background thread</summary>
public abstract class ThreadedDialogViewModel : ThreadedViewModel {
/// <summary>Indicates that the view should close with a positive result</summary>
/// <remarks>
/// This event typically corresponds to the 'Ok' button in a dialog.
/// </remarks>
public event EventHandler Confirmed;
/// <summary>Indicates that the view should close with a negative result</summary>
/// <remarks>
/// This event typically corresponds to the 'Cancel' button in a dialog.
/// </remarks>
public event EventHandler Canceled;
/// <summary>Indicates that the view should close</summary>
/// <remarks>
/// This closes the view with a neutral result, used when the view doesn't follow
/// an ok/cancel scheme or the result is transmitted in some other way.
/// </remarks>
public event EventHandler Submitted;
/// <summary>
/// Indicates that the dialog should be closed with a positive outcome
/// </summary>
public virtual void Confirm() {
if(Confirmed != null) {
Confirmed(this, EventArgs.Empty);
}
}
/// <summary>
/// Indicates that the dialog should be closed with a negative outcome
/// </summary>
public virtual void Cancel() {
if(Canceled != null) {
Canceled(this, EventArgs.Empty);
}
}
/// <summary>Indicates that the dialog should be closed</summary>
public virtual void Submit() {
if(Submitted != null) {
Submitted(this, EventArgs.Empty);
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>View model for a dialog that can execute tasks in a background thread</summary>
public abstract class ThreadedDialogViewModel : ThreadedViewModel {
/// <summary>Indicates that the view should close with a positive result</summary>
/// <remarks>
/// This event typically corresponds to the 'Ok' button in a dialog.
/// </remarks>
public event EventHandler Confirmed;
/// <summary>Indicates that the view should close with a negative result</summary>
/// <remarks>
/// This event typically corresponds to the 'Cancel' button in a dialog.
/// </remarks>
public event EventHandler Canceled;
/// <summary>Indicates that the view should close</summary>
/// <remarks>
/// This closes the view with a neutral result, used when the view doesn't follow
/// an ok/cancel scheme or the result is transmitted in some other way.
/// </remarks>
public event EventHandler Submitted;
/// <summary>
/// Indicates that the dialog should be closed with a positive outcome
/// </summary>
public virtual void Confirm() {
if(Confirmed != null) {
Confirmed(this, EventArgs.Empty);
}
}
/// <summary>
/// Indicates that the dialog should be closed with a negative outcome
/// </summary>
public virtual void Cancel() {
if(Canceled != null) {
Canceled(this, EventArgs.Empty);
}
}
/// <summary>Indicates that the dialog should be closed</summary>
public virtual void Submit() {
if(Submitted != null) {
Submitted(this, EventArgs.Empty);
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels

View file

@ -1,262 +1,261 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2019 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
#if UNITTEST
using System;
using System.ComponentModel;
using System.Threading;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Unit test for the threaded view model base class</summary>
[TestFixture]
public class ThreadedViewModelTest {
#region class DummyContext
/// <summary>Synchronization context that does absolutely nothing</summary>
private class DummyContext : ISynchronizeInvoke {
#region class SimpleAsyncResult
/// <summary>Barebones implementation of an asynchronous result</summary>
private class SimpleAsyncResult : IAsyncResult {
/// <summary>Ehether the asynchronous operation is complete</summary>
/// <remarks>
/// Always true because it completes synchronously
/// </remarks>
public bool IsCompleted { get { return true; } }
/// <summary>
/// Wait handle that can be used to wait for the asynchronous operation
/// </summary>
public WaitHandle AsyncWaitHandle {
get { throw new NotImplementedException("Not implemented"); }
}
/// <summary>Custom state that can be used to pass information around</summary>
public object AsyncState {
get { throw new NotImplementedException("Not implemented"); }
}
/// <summary>Whether the asynchronous operation completed synchronously</summary>
public bool CompletedSynchronously { get { return true; } }
/// <summary>The value returned from the asynchronous operation</summary>
public object ReturnedValue;
}
#endregion // class SimpleAsyncResult
/// <summary>Whether the calling thread needs to use Invoke()</summary>
public bool InvokeRequired {
get { return true; }
}
/// <summary>Schedules the specified method for execution in the target thread</summary>
/// <param name="method">Method the target thread will execute when it is idle</param>
/// <param name="arguments">Arguments that will be passed to the method</param>
/// <returns>
/// An asynchronous result handle that can be used to check on the status of
/// the call and wait for its completion
/// </returns>
public IAsyncResult BeginInvoke(Delegate method, object[] arguments) {
var asyncResult = new SimpleAsyncResult();
asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments);
return asyncResult;
}
/// <summary>Waits for the asychronous call to complete</summary>
/// <param name="result">
/// Asynchronous result handle returned by the <see cref="BeginInvoke" /> method
/// </param>
/// <returns>The original result returned by the asychronously called method</returns>
public object EndInvoke(IAsyncResult result) {
return ((SimpleAsyncResult)result).ReturnedValue;
}
/// <summary>
/// Schedules the specified method for execution in the target thread and waits
/// for it to complete
/// </summary>
/// <param name="method">Method that will be executed by the target thread</param>
/// <param name="arguments">Arguments that will be passed to the method</param>
/// <returns>The result returned by the specified method</returns>
public object Invoke(Delegate method, object[] arguments) {
return method.Method.Invoke(method.Target, arguments);
}
}
#endregion // class DummyContext
#region class TestViewModel
/// <summary>View model used to unit test the threaded view model base class</summary>
private class TestViewModel : ThreadedViewModel {
/// <summary>
/// Initializes a new view model, letting the base class figure out the UI thread
/// </summary>
public TestViewModel() : base() {
this.finishedGate = new ManualResetEvent(initialState: false);
}
/// <summary>
/// Initializes a new view model, using the specified context for the UI thread
/// </summary>
/// <param name="uiContext">Synchronization context of the UI thread</param>
public TestViewModel(ISynchronizeInvoke uiContext) : base(uiContext) {
this.finishedGate = new ManualResetEvent(initialState: false);
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public override void Dispose() {
base.Dispose();
if(this.finishedGate != null) {
this.finishedGate.Dispose();
this.finishedGate = null;
}
}
/// <summary>Waits until the first background operation is finished</summary>
/// <returns>
/// True if the background operation is finished, false if it is ongoing
/// </returns>
public bool WaitUntilFinished() {
return this.finishedGate.WaitOne(100);
}
/// <summary>Runs a background process that causes the specified error</summary>
/// <param name="error">Error that will be caused in the background process</param>
public void CauseErrorInBackgroundThread(Exception error) {
RunInBackground(
delegate() { throw error; }
);
}
/// <summary>
/// Assigns the specified value to the same-named property from a background thread
/// </summary>
/// <param name="value">Value that will be assigned to the same-named property</param>
public void AssignValueInBackgroundThread(int value) {
RunInBackground(
delegate () {
this.assignedValue = value;
this.finishedGate.Set();
}
);
}
/// <summary>Last error that was reported by the threaded view model</summary>
public Exception ReportedError {
get { return this.reportedError; }
}
/// <summary>Value that has been assigned from the background thread</summary>
public int AssignedValue {
get { return this.assignedValue; }
}
/// <summary>Called when an error occurs in the background thread</summary>
/// <param name="exception">Exception that was thrown in the background thread</param>
protected override void ReportError(Exception exception) {
this.reportedError = exception;
this.finishedGate.Set();
}
/// <summary>Last error that was reported by the threaded view model</summary>
private volatile Exception reportedError;
/// <summary>Triggered when the </summary>
private ManualResetEvent finishedGate;
/// <summary>Value that is assigned through the background thread</summary>
private volatile int assignedValue;
}
#endregion // class TestViewModel
/// <summary>Verifies that the threaded view model has a default constructor</summary>
[Test, Explicit]
public void HasDefaultConstructor() {
using(var mainForm = new System.Windows.Forms.Form()) {
mainForm.Show();
try {
mainForm.Visible = false;
using(new TestViewModel()) { }
}
finally {
mainForm.Close();
}
}
}
/// <summary>
/// Verifies that the threaded view model can be constructed with a custom UI context
/// </summary>
[Test]
public void HasCustomSychronizationContextConstructor() {
using(new TestViewModel(new DummyContext())) { }
}
/// <summary>Checks that a new view model starts out idle and not busy</summary>
[Test]
public void NewInstanceIsNotBusy() {
using(var viewModel = new TestViewModel(new DummyContext())) {
Assert.IsFalse(viewModel.IsBusy);
}
}
/// <summary>
/// Verifies that errors happening in the background processing threads are
/// reported to the main thread
/// </summary>
[Test]
public void ErrorsInBackgroundThreadAreReported() {
using(var viewModel = new TestViewModel(new DummyContext())) {
var testError = new ArgumentException("Mooh");
viewModel.CauseErrorInBackgroundThread(testError);
viewModel.WaitUntilFinished();
Assert.AreSame(testError, viewModel.ReportedError);
}
}
/// <summary>
/// Verifies that the background thread actually executes and can do work
/// </summary>
[Test]
public void BackgroundThreadExecutesTasks() {
using(var viewModel = new TestViewModel(new DummyContext())) {
viewModel.AssignValueInBackgroundThread(10042);
viewModel.WaitUntilFinished();
Assert.AreEqual(10042, viewModel.AssignedValue);
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
#if UNITTEST
using System;
using System.ComponentModel;
using System.Threading;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Unit test for the threaded view model base class</summary>
[TestFixture]
public class ThreadedViewModelTest {
#region class DummyContext
/// <summary>Synchronization context that does absolutely nothing</summary>
private class DummyContext : ISynchronizeInvoke {
#region class SimpleAsyncResult
/// <summary>Barebones implementation of an asynchronous result</summary>
private class SimpleAsyncResult : IAsyncResult {
/// <summary>Ehether the asynchronous operation is complete</summary>
/// <remarks>
/// Always true because it completes synchronously
/// </remarks>
public bool IsCompleted { get { return true; } }
/// <summary>
/// Wait handle that can be used to wait for the asynchronous operation
/// </summary>
public WaitHandle AsyncWaitHandle {
get { throw new NotImplementedException("Not implemented"); }
}
/// <summary>Custom state that can be used to pass information around</summary>
public object AsyncState {
get { throw new NotImplementedException("Not implemented"); }
}
/// <summary>Whether the asynchronous operation completed synchronously</summary>
public bool CompletedSynchronously { get { return true; } }
/// <summary>The value returned from the asynchronous operation</summary>
public object ReturnedValue;
}
#endregion // class SimpleAsyncResult
/// <summary>Whether the calling thread needs to use Invoke()</summary>
public bool InvokeRequired {
get { return true; }
}
/// <summary>Schedules the specified method for execution in the target thread</summary>
/// <param name="method">Method the target thread will execute when it is idle</param>
/// <param name="arguments">Arguments that will be passed to the method</param>
/// <returns>
/// An asynchronous result handle that can be used to check on the status of
/// the call and wait for its completion
/// </returns>
public IAsyncResult BeginInvoke(Delegate method, object[] arguments) {
var asyncResult = new SimpleAsyncResult();
asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments);
return asyncResult;
}
/// <summary>Waits for the asychronous call to complete</summary>
/// <param name="result">
/// Asynchronous result handle returned by the <see cref="BeginInvoke" /> method
/// </param>
/// <returns>The original result returned by the asychronously called method</returns>
public object EndInvoke(IAsyncResult result) {
return ((SimpleAsyncResult)result).ReturnedValue;
}
/// <summary>
/// Schedules the specified method for execution in the target thread and waits
/// for it to complete
/// </summary>
/// <param name="method">Method that will be executed by the target thread</param>
/// <param name="arguments">Arguments that will be passed to the method</param>
/// <returns>The result returned by the specified method</returns>
public object Invoke(Delegate method, object[] arguments) {
return method.Method.Invoke(method.Target, arguments);
}
}
#endregion // class DummyContext
#region class TestViewModel
/// <summary>View model used to unit test the threaded view model base class</summary>
private class TestViewModel : ThreadedViewModel {
/// <summary>
/// Initializes a new view model, letting the base class figure out the UI thread
/// </summary>
public TestViewModel() : base() {
this.finishedGate = new ManualResetEvent(initialState: false);
}
/// <summary>
/// Initializes a new view model, using the specified context for the UI thread
/// </summary>
/// <param name="uiContext">Synchronization context of the UI thread</param>
public TestViewModel(ISynchronizeInvoke uiContext) : base(uiContext) {
this.finishedGate = new ManualResetEvent(initialState: false);
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public override void Dispose() {
base.Dispose();
if(this.finishedGate != null) {
this.finishedGate.Dispose();
this.finishedGate = null;
}
}
/// <summary>Waits until the first background operation is finished</summary>
/// <returns>
/// True if the background operation is finished, false if it is ongoing
/// </returns>
public bool WaitUntilFinished() {
return this.finishedGate.WaitOne(100);
}
/// <summary>Runs a background process that causes the specified error</summary>
/// <param name="error">Error that will be caused in the background process</param>
public void CauseErrorInBackgroundThread(Exception error) {
RunInBackground(
delegate() { throw error; }
);
}
/// <summary>
/// Assigns the specified value to the same-named property from a background thread
/// </summary>
/// <param name="value">Value that will be assigned to the same-named property</param>
public void AssignValueInBackgroundThread(int value) {
RunInBackground(
delegate () {
this.assignedValue = value;
this.finishedGate.Set();
}
);
}
/// <summary>Last error that was reported by the threaded view model</summary>
public Exception ReportedError {
get { return this.reportedError; }
}
/// <summary>Value that has been assigned from the background thread</summary>
public int AssignedValue {
get { return this.assignedValue; }
}
/// <summary>Called when an error occurs in the background thread</summary>
/// <param name="exception">Exception that was thrown in the background thread</param>
protected override void ReportError(Exception exception) {
this.reportedError = exception;
this.finishedGate.Set();
}
/// <summary>Last error that was reported by the threaded view model</summary>
private volatile Exception reportedError;
/// <summary>Triggered when the </summary>
private ManualResetEvent finishedGate;
/// <summary>Value that is assigned through the background thread</summary>
private volatile int assignedValue;
}
#endregion // class TestViewModel
/// <summary>Verifies that the threaded view model has a default constructor</summary>
[Test, Explicit]
public void HasDefaultConstructor() {
using(var mainForm = new System.Windows.Forms.Form()) {
mainForm.Show();
try {
mainForm.Visible = false;
using(new TestViewModel()) { }
}
finally {
mainForm.Close();
}
}
}
/// <summary>
/// Verifies that the threaded view model can be constructed with a custom UI context
/// </summary>
[Test]
public void HasCustomSychronizationContextConstructor() {
using(new TestViewModel(new DummyContext())) { }
}
/// <summary>Checks that a new view model starts out idle and not busy</summary>
[Test]
public void NewInstanceIsNotBusy() {
using(var viewModel = new TestViewModel(new DummyContext())) {
Assert.IsFalse(viewModel.IsBusy);
}
}
/// <summary>
/// Verifies that errors happening in the background processing threads are
/// reported to the main thread
/// </summary>
[Test]
public void ErrorsInBackgroundThreadAreReported() {
using(var viewModel = new TestViewModel(new DummyContext())) {
var testError = new ArgumentException("Mooh");
viewModel.CauseErrorInBackgroundThread(testError);
viewModel.WaitUntilFinished();
Assert.AreSame(testError, viewModel.ReportedError);
}
}
/// <summary>
/// Verifies that the background thread actually executes and can do work
/// </summary>
[Test]
public void BackgroundThreadExecutesTasks() {
using(var viewModel = new TestViewModel(new DummyContext())) {
viewModel.AssignValueInBackgroundThread(10042);
viewModel.WaitUntilFinished();
Assert.AreEqual(10042, viewModel.AssignedValue);
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
#endif // UNITTEST

View file

@ -1,232 +1,231 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2019 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.ComponentModel;
using System.Windows.Forms;
using Nuclex.Support;
using Nuclex.Support.Threading;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>View model that can execute tasks in a background thread</summary>
public abstract class ThreadedViewModel : Observable, IDisposable {
#region class ViewModelThreadRunner
/// <summary>Thread runner for the threaded view model</summary>
private class ViewModelThreadRunner : ThreadRunner {
/// <summary>Initializes a new thread runner for the threaded view model</summary>
public ViewModelThreadRunner(ThreadedViewModel viewModel) {
this.viewModel = viewModel;
}
/// <summary>Reports an error</summary>
/// <param name="exception">Error that will be reported</param>
protected override void ReportError(Exception exception) {
this.viewModel.reportErrorFromThread(exception);
}
/// <summary>Called when the status of the busy flag changes</summary>
protected override void BusyChanged() {
this.viewModel.OnIsBusyChanged();
}
/// <summary>View model the thread runner belongs to</summary>
private ThreadedViewModel viewModel;
}
#endregion // class ViewModelThreadRunner
/// <summary>Initializes a new view model for background processing</summary>
/// <param name="uiContext">
/// UI dispatcher that can be used to run callbacks in the UI thread
/// </param>
protected ThreadedViewModel(ISynchronizeInvoke uiContext = null) {
if(uiContext == null) {
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
if(this.uiContext == null) {
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
}
} else {
this.uiContext = uiContext;
}
this.reportErrorDelegate = new Action<Exception>(ReportError);
this.threadRunner = new ViewModelThreadRunner(this);
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public virtual void Dispose() {
if(this.threadRunner != null) {
this.threadRunner.Dispose();
this.threadRunner = null;
}
}
/// <summary>Whether the view model is currently busy executing a task</summary>
public bool IsBusy {
get { return this.threadRunner.IsBusy; }
}
/// <summary>Reports an error to the user</summary>
/// <param name="exception">Error that will be reported</param>
/// <remarks>
/// <para>
/// You can use this method as a default handling method for your own error reporting
/// (displaying the error to the user, logging it or whatever else is appropriate).
/// </para>
/// <para>
/// When <see cref="RunInBackground(System.Action)" /> is used, this method will also
/// be called in case an exception within the asynchronously running code goes unhandled.
/// This choice was made because, in the context of UI code, you would wrap any
/// operations that might fail in a try..catch pair anyway in order to inform
/// the user instead of aborting the entire application.
/// </para>
/// </remarks>
protected abstract void ReportError(Exception exception);
/// <summary>Executes the specified operation in the background</summary>
/// <param name="action">Action that will be executed in the background</param>
protected void RunInBackground(Action action) {
this.threadRunner.RunInBackground(action);
}
/// <summary>Executes the specified operation in the background</summary>
/// <param name="action">Action that will be executed in the background</param>
protected void RunInBackground(CancellableAction action) {
this.threadRunner.RunInBackground(action);
}
/// <summary>Executes the specified operation in the background</summary>
/// <param name="action">Action that will be executed in the background</param>
/// <param name="parameter1">Parameter that will be passed to the action</param>
protected void RunInBackground<P1>(Action<P1> action, P1 parameter1) {
this.threadRunner.RunInBackground(action, parameter1);
}
/// <summary>Executes the specified operation in the background</summary>
/// <param name="action">Action that will be executed in the background</param>
/// <param name="parameter1">Parameter that will be passed to the action</param>
protected void RunInBackground<P1>(CancellableAction<P1> action, P1 parameter1) {
this.threadRunner.RunInBackground(action, parameter1);
}
/// <summary>Executes the specified operation in the background</summary>
/// <param name="action">Action that will be executed in the background</param>
/// <param name="parameter1">First parameter that will be passed to the action</param>
/// <param name="parameter2">Second parameter that will be passed to the action</param>
protected void RunInBackground<P1, P2>(Action<P1, P2> action, P1 parameter1, P2 parameter2) {
this.threadRunner.RunInBackground(action, parameter1, parameter2);
}
/// <summary>Executes the specified operation in the background</summary>
/// <param name="action">Action that will be executed in the background</param>
/// <param name="parameter1">First parameter that will be passed to the action</param>
/// <param name="parameter2">Second parameter that will be passed to the action</param>
protected void RunInBackground<P1, P2>(
CancellableAction<P1, P2> action, P1 parameter1, P2 parameter2
) {
this.threadRunner.RunInBackground(action, parameter1, parameter2);
}
/// <summary>Cancels the currently running background operation</summary>
protected void CancelBackgroundOperation() {
this.threadRunner.CancelBackgroundOperation();
}
/// <summary>Cancels all queued and the currently running background operation</summary>
protected void CancelAllBackgroundOperations() {
this.threadRunner.CancelAllBackgroundOperations();
}
/// <summary>Whether the background operation has been cancelled</summary>
//[Obsolete("Please use a method accepting a cancellation token instead of using this")]
protected bool IsBackgroundOperationCancelled {
get { return this.threadRunner.IsBackgroundOperationCancelled; }
}
/// <summary>Throws an exception if the background operation was cancelled</summary>
//[Obsolete("Please use a method accepting a cancellation token instead of using this")]
protected void ThrowIfBackgroundOperationCancelled() {
this.threadRunner.ThrowIfBackgroundOperationCancelled();
}
/// <summary>Executes the specified action in the UI thread</summary>
/// <param name="action">Action that will be executed in the UI thread</param>
protected void RunInUIThread(Action action) {
this.uiContext.Invoke(action, EmptyObjectArray);
}
/// <summary>Executes the specified action in the UI thread</summary>
/// <param name="action">Action that will be executed in the UI thread</param>
/// <param name="parameter1">Parameter that will be passed to the action</param>
protected void RunInUIThread<P1>(Action<P1> action, P1 parameter1) {
this.uiContext.Invoke(action, new object[1] { parameter1 });
}
/// <summary>Executes the specified action in the UI thread</summary>
/// <param name="action">Action that will be executed in the UI thread</param>
/// <param name="parameter1">First parameter that will be passed to the action</param>
/// <param name="parameter2">Second parameter that will be passed to the action</param>
protected void RunInUIThread<P1, P2>(Action<P1, P2> action, P1 parameter1, P2 parameter2) {
this.uiContext.Invoke(action, new object[2] { parameter1, parameter2 });
}
/// <summary>Called when the thread runner's busy flag changes</summary>
protected virtual void OnIsBusyChanged() {
OnPropertyChanged(nameof(IsBusy));
}
// For the ThreadedAction class - there should be a better way!
/// <summary>Thread runner that manages the view model's thread</summary>
internal ThreadRunner ThreadRunner {
get { return this.threadRunner; }
}
/// <summary>Reports an error that occurred in the runner's background thread</summary>
/// <param name="exception">Exception that the thread has encountered</param>
private void reportErrorFromThread(Exception exception) {
this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception });
}
/// <summary>Sets the UI context that will be used by the threaded action</summary>
/// <param name="uiContext">The UI context the threaded action will use</param>
private void updateUiContext(ISynchronizeInvoke uiContext) {
this.uiContext = uiContext;
}
/// <summary>An array of zero objects</summary>
private static readonly object[] EmptyObjectArray = new object[0];
/// <summary>UI dispatcher of the thread in which the view runs</summary>
private ISynchronizeInvoke uiContext;
/// <summary>Delegate for the ReportError() method</summary>
private Action<Exception> reportErrorDelegate;
/// <summary>Thread runner that manages the view model's thread</summary>
private ViewModelThreadRunner threadRunner;
}
} // namespace Nuclex.Windows.Forms.ViewModels
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using System.ComponentModel;
using System.Windows.Forms;
using Nuclex.Support;
using Nuclex.Support.Threading;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>View model that can execute tasks in a background thread</summary>
public abstract class ThreadedViewModel : Observable, IDisposable {
#region class ViewModelThreadRunner
/// <summary>Thread runner for the threaded view model</summary>
private class ViewModelThreadRunner : ThreadRunner {
/// <summary>Initializes a new thread runner for the threaded view model</summary>
public ViewModelThreadRunner(ThreadedViewModel viewModel) {
this.viewModel = viewModel;
}
/// <summary>Reports an error</summary>
/// <param name="exception">Error that will be reported</param>
protected override void ReportError(Exception exception) {
this.viewModel.reportErrorFromThread(exception);
}
/// <summary>Called when the status of the busy flag changes</summary>
protected override void BusyChanged() {
this.viewModel.OnIsBusyChanged();
}
/// <summary>View model the thread runner belongs to</summary>
private ThreadedViewModel viewModel;
}
#endregion // class ViewModelThreadRunner
/// <summary>Initializes a new view model for background processing</summary>
/// <param name="uiContext">
/// UI dispatcher that can be used to run callbacks in the UI thread
/// </param>
protected ThreadedViewModel(ISynchronizeInvoke uiContext = null) {
if(uiContext == null) {
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
if(this.uiContext == null) {
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
}
} else {
this.uiContext = uiContext;
}
this.reportErrorDelegate = new Action<Exception>(ReportError);
this.threadRunner = new ViewModelThreadRunner(this);
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public virtual void Dispose() {
if(this.threadRunner != null) {
this.threadRunner.Dispose();
this.threadRunner = null;
}
}
/// <summary>Whether the view model is currently busy executing a task</summary>
public bool IsBusy {
get { return this.threadRunner.IsBusy; }
}
/// <summary>Reports an error to the user</summary>
/// <param name="exception">Error that will be reported</param>
/// <remarks>
/// <para>
/// You can use this method as a default handling method for your own error reporting
/// (displaying the error to the user, logging it or whatever else is appropriate).
/// </para>
/// <para>
/// When <see cref="RunInBackground(System.Action)" /> is used, this method will also
/// be called in case an exception within the asynchronously running code goes unhandled.
/// This choice was made because, in the context of UI code, you would wrap any
/// operations that might fail in a try..catch pair anyway in order to inform
/// the user instead of aborting the entire application.
/// </para>
/// </remarks>
protected abstract void ReportError(Exception exception);
/// <summary>Executes the specified operation in the background</summary>
/// <param name="action">Action that will be executed in the background</param>
protected void RunInBackground(Action action) {
this.threadRunner.RunInBackground(action);
}
/// <summary>Executes the specified operation in the background</summary>
/// <param name="action">Action that will be executed in the background</param>
protected void RunInBackground(CancellableAction action) {
this.threadRunner.RunInBackground(action);
}
/// <summary>Executes the specified operation in the background</summary>
/// <param name="action">Action that will be executed in the background</param>
/// <param name="parameter1">Parameter that will be passed to the action</param>
protected void RunInBackground<P1>(Action<P1> action, P1 parameter1) {
this.threadRunner.RunInBackground(action, parameter1);
}
/// <summary>Executes the specified operation in the background</summary>
/// <param name="action">Action that will be executed in the background</param>
/// <param name="parameter1">Parameter that will be passed to the action</param>
protected void RunInBackground<P1>(CancellableAction<P1> action, P1 parameter1) {
this.threadRunner.RunInBackground(action, parameter1);
}
/// <summary>Executes the specified operation in the background</summary>
/// <param name="action">Action that will be executed in the background</param>
/// <param name="parameter1">First parameter that will be passed to the action</param>
/// <param name="parameter2">Second parameter that will be passed to the action</param>
protected void RunInBackground<P1, P2>(Action<P1, P2> action, P1 parameter1, P2 parameter2) {
this.threadRunner.RunInBackground(action, parameter1, parameter2);
}
/// <summary>Executes the specified operation in the background</summary>
/// <param name="action">Action that will be executed in the background</param>
/// <param name="parameter1">First parameter that will be passed to the action</param>
/// <param name="parameter2">Second parameter that will be passed to the action</param>
protected void RunInBackground<P1, P2>(
CancellableAction<P1, P2> action, P1 parameter1, P2 parameter2
) {
this.threadRunner.RunInBackground(action, parameter1, parameter2);
}
/// <summary>Cancels the currently running background operation</summary>
protected void CancelBackgroundOperation() {
this.threadRunner.CancelBackgroundOperation();
}
/// <summary>Cancels all queued and the currently running background operation</summary>
protected void CancelAllBackgroundOperations() {
this.threadRunner.CancelAllBackgroundOperations();
}
/// <summary>Whether the background operation has been cancelled</summary>
//[Obsolete("Please use a method accepting a cancellation token instead of using this")]
protected bool IsBackgroundOperationCancelled {
get { return this.threadRunner.IsBackgroundOperationCancelled; }
}
/// <summary>Throws an exception if the background operation was cancelled</summary>
//[Obsolete("Please use a method accepting a cancellation token instead of using this")]
protected void ThrowIfBackgroundOperationCancelled() {
this.threadRunner.ThrowIfBackgroundOperationCancelled();
}
/// <summary>Executes the specified action in the UI thread</summary>
/// <param name="action">Action that will be executed in the UI thread</param>
protected void RunInUIThread(Action action) {
this.uiContext.Invoke(action, EmptyObjectArray);
}
/// <summary>Executes the specified action in the UI thread</summary>
/// <param name="action">Action that will be executed in the UI thread</param>
/// <param name="parameter1">Parameter that will be passed to the action</param>
protected void RunInUIThread<P1>(Action<P1> action, P1 parameter1) {
this.uiContext.Invoke(action, new object[1] { parameter1 });
}
/// <summary>Executes the specified action in the UI thread</summary>
/// <param name="action">Action that will be executed in the UI thread</param>
/// <param name="parameter1">First parameter that will be passed to the action</param>
/// <param name="parameter2">Second parameter that will be passed to the action</param>
protected void RunInUIThread<P1, P2>(Action<P1, P2> action, P1 parameter1, P2 parameter2) {
this.uiContext.Invoke(action, new object[2] { parameter1, parameter2 });
}
/// <summary>Called when the thread runner's busy flag changes</summary>
protected virtual void OnIsBusyChanged() {
OnPropertyChanged(nameof(IsBusy));
}
// For the ThreadedAction class - there should be a better way!
/// <summary>Thread runner that manages the view model's thread</summary>
internal ThreadRunner ThreadRunner {
get { return this.threadRunner; }
}
/// <summary>Reports an error that occurred in the runner's background thread</summary>
/// <param name="exception">Exception that the thread has encountered</param>
private void reportErrorFromThread(Exception exception) {
this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception });
}
/// <summary>Sets the UI context that will be used by the threaded action</summary>
/// <param name="uiContext">The UI context the threaded action will use</param>
private void updateUiContext(ISynchronizeInvoke uiContext) {
this.uiContext = uiContext;
}
/// <summary>An array of zero objects</summary>
private static readonly object[] EmptyObjectArray = new object[0];
/// <summary>UI dispatcher of the thread in which the view runs</summary>
private ISynchronizeInvoke uiContext;
/// <summary>Delegate for the ReportError() method</summary>
private Action<Exception> reportErrorDelegate;
/// <summary>Thread runner that manages the view model's thread</summary>
private ViewModelThreadRunner threadRunner;
}
} // namespace Nuclex.Windows.Forms.ViewModels