Reputation: 4786
I have a series of ObservableCollection<string>
s which are each bound to their own listbox
. I have four worker threads, which each need to be able to add to these collections. The problem is, I can't add to these collections from the non-UI thread.
Normally (if I wasn't using data-binding) I'd use something like:
private delegate void ProgressBarStepConsumer(ProgressBar pBar);
public static void ProgressBarTakeStep(ProgressBar pBar)
{
if (pBar.InvokeRequired)
{
pBar.Invoke(new ProgressBarStepConsumer(ProgressBarTakeStep), pBar);
}
else
{
lock (pBar)
{
pBar.PerformStep();
}
}
}
However, since I'm not accessing the control directly, I don't know how I'd go about doing this, so any advice would be helpful.
My previous (incorrect attempt) looked like this:
switch (Path.GetFileNameWithoutExtension(file).Substring(Path.GetFileNameWithoutExtension(file).Length - 2, 2))
{
case "UL":
{
lock (_ulFileList)
{
_ulFileList.Add(Path.GetFileName(file));
}
break;
}
case "UR":
{
lock (_urFileList)
{
_urFileList.Add(Path.GetFileName(file));
}
break;
}
case "LR":
{
lock (_lrFileList)
{
_lrFileList.Add(Path.GetFileName(file));
}
break;
}
case "LL":
{
lock (_llFileList)
{
_llFileList.Add(Path.GetFileName(file));
}
break;
}
}
And my Thread
structure is as such:
Thread ulThread = new Thread(() => ConverterWorker(ulQueue, Corner.UL, destPath));
Thread urThread = new Thread(() => ConverterWorker(urQueue, Corner.UR, destPath));
Thread lrThread = new Thread(() => ConverterWorker(lrQueue, Corner.LR, destPath));
Thread llThread = new Thread(() => ConverterWorker(llQueue, Corner.LL, destPath));
Where ConverterWorker()
is a private void
method.
Whilst researching my question beforehand I realised that having a cross thread operation for every iteration (that switch
is inside a loop) might be very inefficient, so I'll store the changes locally and then update the UI in bigger batches, but I can work that out myself once I can access these collections.
Upvotes: 0
Views: 63
Reputation: 14007
The problem you are facing is that thread synchronization is an UI issue, while the work you are doing is part of the business logic. In a well-designed system, the business logic has no knowledge of the UI and so it can not know the synchronization specifics. The solution to get out of the situation of "the business logic must synchronize through the UI logic, but it should have no knowledge of the UI" is devendency inversion.
Dependency inversion is a common design practice. In fact is is so common that it is the D in the SOLID principles. In your case it means that the dependency of the business logic on the UI synchronization logic is inversed, so the UI logic is dependent on the business logic, which is the correct design approach, since the UI layer is on top of the business layer.
It is recommended to put the business concerns into a different assembly than the UI concerns. The UI assembly references the business assembly, which in turn should have no references to any assemblies related to specific UI technology (such as any of the System.Web
or System.Windows
assemblies). That however makes Application.Current.Dispatcher.Invoke
inaccessible to you. This is actually a good thing, because this is a part of the UI layer, which the business logic should have no knowledge of. There are two possible ways to solve the synchronization dilemma you are facing now:
UI independent abstraction
In the System.ComponentModel
namespace you will find the ISynchronizeInvoke
interface, which is used for your case where calls need to be synchronized. Although this interface is used extensively in Windows Forms, it is actually UI unspecific (and not defined in any UI specific assembly). So you can safely introduce a dependency to this inteface in your business class. Assuming the ConverterWorker
method is defined in the MyBusinessClass
class, you could pass an implementation of the interface in the constructor:
public class MyBusinessClass {
private ISynchronizeInvoke syncInv;
public MyBusinessClass(ISynchronizeInvoke syncInv) {
this.syncInv = syncInv;
}
}
You can now do all operations that need to be synchronized through syncInv
and synchronize independently of the specific UI technology. While in Windows Forms all controls (inclding the form) are ISynchronizeInvoke
implementations, you can simply pass the form when synchronizing the business object. In WPF you have to implement the interace yourself, which is trivial, since you only have to call Application.Current.Dispatcher.Invoke
in the Invoke
method (and do the same respectively for BeginInvoke
etc.).
Dependency injection
With dependency incjection you don't pass the abstraction directly to your business class, but the higher layer code (in your case the UI) "injects" the dependency code to the lower layer, which its classes can pick up and use. There are plenty of existing dependency injection frameworks, but you can implement a simple form of dependency injection yourself, which serves you well in simple scenarios. In your business logic (or an independent framework used by both layers) you can define this:
public static class DependencyManager {
private static Dictionary<Type, object> dependencies = new Dictionary<Type, object>();
public static void AddDependency<TInterf, TImpl>(TImpl dependency)
where TImpl : TInterf {
dependencies[typeof(TInterf)] = dependency;
}
public static T GetDependency<T>() {
T dependency;
bool hasDependency = dependencies.TryGetValue(typeof(T), out dependency);
if (hasDependency) {
return dependency;
}
else {
return default(T)
}
}
}
The business layer can get the ISynchronizeInvoke
like this:
ISynchronizeInvoke syncInv = DependencyManager.GetDependency<ISynchronizeInvoke>();
The UI layer can inject the dependency like this (this has to happen before using business objects that rely on the dependency):
DependencyManager.AddDependency<ISynchronizeInvoke, MySyncInv>(mySyncInvImplementation);
Upvotes: 3
Reputation: 4786
Since asking this question I have worked out that one can Dispatch
a task to be executed by the UI thread.
This is done like so:
Application.Current.Dispatcher.Invoke(() =>
{
_ulFileList.Add(Path.GetFileName(file));
});
Upvotes: 0