Executing .DataSource on the GUI thread

Using another thread to update GUI elements

Page 1 of 1

6 Replies - 9010 Views - Last Post: 10 May 2009 - 06:18 AM Rate Topic: -----

#1 Methical   User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 8
  • Joined: 08-May 09

Executing .DataSource on the GUI thread

Post icon  Posted 08 May 2009 - 10:53 AM

Hey guys, first post here so I hope someone can help.

I am having a problem with updating a DataGridView's, DataSource property from a thread that is not the one it was created on. I have done many searches both on this forum and on the web and there seems to be two ways people suggest to go about it.

1. Use a BackgroundWorker control
2. Use a combination of .Invoke() and a delegate system.

Problem is, the BackgroundWorker control didn't seem to work at all and I really am having issues getting my head around this delegate business. I have used them once for routing events but I only JUST understood that and using delegates in this way is far beyond me. Yes, I have found and read the "Cross Thread Communication in C#" tutorial posted on here but being as I don't understand the delegate system, I found it impossible to transfer the content to my own project. To this end, I ask you kind forum users to maybe shed some light on my issue.

I am writing this WinForm program as a means of better learning the MVP design pattern. I politely request that you refrain from commenting on how well i have implemented this pattern as this is not the subject of this post. So here's my issue.

Within my solution are three projects, FolderSync.View, FolderSync.Presenters & FolderSync.Model. I am trying to update the DataGridView in View with data the Presenter has retrieved from the model. So i don't have to post too much code, it's worth mentioning that Watch.cs and Observation.cs are classes in my Model. Here is the code.

FolderSync.View - Mainwindow.cs
 

public partial class MainWindow : Form, IMainView
{
	Presnter_MainView presenter;
	
	Public MainWindow()
	{
		// Init stuff
		presenter = new Presenter_MainWindow(this);
	}

	#region IMainView members
	
	 public List<Watch> WatchGridDataSource
	 {
		  set
		  {
			   dgWatch.DataSource = value;
		  }
	 }

	 public List<Observation> ObservationGridDataSource
	 {
		  set
		  {
			   dgObservation.DataSource = value;
		  }
	 }

	 #endregion




}



So there's the GUI stuff. Now a few bits to explain. Basically, every Watch.cs has a FileSystemWatcher control in it and when that kicks up and event, an Observation.cs is made and added to a List<Observation> within the watch object. Periodically (every 5000ms) a class Synchroniser.cs checks the contents of each Watch's observation collection and adds new entries to its own collection of Observations from every existing Watch object. A second thread, located in the Presenter class (Presenter_Mainwindow.cs) checks the Synchroniser.cs collection of watches and observations and attempts to bind these lists as the DataSource of a DataGridView in the View. For clarity, the Presenter does not know it is 'talking' to a MainWindow, all it knows is that whatever is passed in to its constructor is going to be an instance of IMainView and thus will have WatchGridData and ObservationGridData properties.

So what i originally wanted to do was the following;

FolderSync.Presenters - Presenter_Mainwindow.cs

	public class Presenter_MainView
	{
		IMainView view;
		Synchroniser syncEngine;

		Timer timer_checkModel;
		TimerCallback timerCb;

		public Presenter_MainView(IMainView view)
		{
			this.view = view;
			syncEngine = new Synchroniser();

			timerCb = new TimerCallback(checkModel);
			timer_checkModel = new Timer(timerCb, null, 1000, 5000);  
		}

		private void checkModel(object state)
		{
			List<Watch> watches = syncEngine.Watches;
			List<Observation> observations = new List<Observation>();

			for (int i = 0; i < watches.Count; i++)
			{
				Watch w = watches[i];
				foreach (Observation obs in w.observations)
				{
					observations.Add(obs);
				}
			}

			UpdateGridData(watches, observations);
		}

		 private void UpdateGridData(List<Watch> watchData, List<Observation> obsData)
		{
			view.WatchGridDataSource = watchData;
			view.ObservationGridDataSource = obsData;
		}
	}




It is within the "UpdateGridData()" method where the issue occurs. Each time this method is called, I get an 'InvalidOperationException' and it states that the DataGridView control cannot be manipulated outside of the thread it was created on. Again, it seems delegates are the way to go but I just dont get what's happening on the process and where do all these Delegate thingy's need to be declared ? In the presenter or in the view ? Or half and half or what !?!?

I know it's again policy to "give me code" but at least some explanation as to where and why to put these delegate properties into my project would be a god-send.

Thank you for your time on this matter, if I have left anything out I apologise, comment on it and I will try and update the post.

Is This A Good Question/Topic? 0
  • +

Replies To: Executing .DataSource on the GUI thread

#2 beatles1692   User is offline

  • D.I.C Head

Reputation: 13
  • View blog
  • Posts: 62
  • Joined: 03-December 08

Re: Executing .DataSource on the GUI thread

Posted 08 May 2009 - 09:12 PM

Hi
I'm not sure if it's going to work (I never tried it on different threads) .Why don't you use BindingSource as DataSource of your grids and then you can change the grid data source using the BindingSource object.

Here's the msdn document.

This post has been edited by beatles1692: 08 May 2009 - 09:18 PM

Was This Post Helpful? 0
  • +
  • -

#3 Methical   User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 8
  • Joined: 08-May 09

Re: Executing .DataSource on the GUI thread

Posted 09 May 2009 - 04:16 AM

Thanks for the idea. I may need correcting if i have implemented it incorrectly but i did the following;

Added BindingSource from the toolbox to my WinForm.
Used the wizard to bind it to a Watch.cs object
then changed Mainwindow.cs as to;

		 public List<Watch> WatchGridDataSource
		{
			set
			{
					foreach(Watch w in value)
					{
						bindingSource1.Add(w);
					}
					dgWatch.DataSource = bindingSource1;					
			}
		}



Sadly though as the Presenter is on a separate thread to the GUI, when i use the presenter as follows

		private void UpdateGridData(List<Watch> watchData, List<Observation> obsData)
		{
			if(watchData.Count > 0)
			{
				view.WatchGridDataSource = watchData;
			}
			if(obsData.Count > 0)
			{
				view.ObservationGridDataSource = obsData; 
			}
		}



It still complains that i am accessing a control on a thread that it was not created on ! Makes sense. The only way I can see me getting this to work is to actually declare and create the DataGridView within the presenter and send it to the view as and when it is needed. I don't know for sure that would work though and if it did, it would be a horrible hack-ish way of doing things and id really rather avoid it. Anyone care to explain the delegate system a bit more.....simply ? The delegate system definitely seems the best way to go according to popular opinion.
Was This Post Helpful? 0
  • +
  • -

#4 beatles1692   User is offline

  • D.I.C Head

Reputation: 13
  • View blog
  • Posts: 62
  • Joined: 03-December 08

Re: Executing .DataSource on the GUI thread

Posted 09 May 2009 - 04:53 AM

As fat as I can see , the presenter is simply an object (it's not inherited from any controls or components) then there should be no problem accessing its properties in a different thread. I think that you should cut the dependency between your presenter and DataGridViews and let it know the BindingSources instead.This way you can set your data gird views to the presenter binding sources in your main thread and update the binding source objects in another thread and hopefully it will work.
Here's what I mean:

private void UpdateGridData(List<Watch> watchData, List<Observation> obsData)
		{
			if(watchData.Count > 0)
			{
				// instead of view.WatchGridDataSource = watchData;
				view.WatchBindingSource=watchData;
			}
			if(obsData.Count > 0)
			{
				//instead of view.ObservationGridDataSource = obsData;
			   view.ObservationBindingSource=obsData;
			}
		}


Was This Post Helpful? 0
  • +
  • -

#5 Methical   User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 8
  • Joined: 08-May 09

Re: Executing .DataSource on the GUI thread

Posted 09 May 2009 - 01:20 PM

View Postbeatles1692, on 9 May, 2009 - 03:53 AM, said:

As fat as I can see , the presenter is simply an object (it's not inherited from any controls or components) then there should be no problem accessing its properties in a different thread.


Im not modifying the presenters properties from another thread, im modifying a GUI element running on another thread FROM the presenter.

Quote

I think that you should cut the dependency between your presenter and DataGridViews and let it know the BindingSources instead.This way you can set your data gird views to the presenter binding sources in your main thread and update the binding source objects in another thread and hopefully it will work.


I did exactly that, Now the properties in view are as follows:

FolderSync.View - Mainwindow.cs (implements IMainView)
public partial class MainWindow : Form, IMainView
{
		public Presenter_MainView presenter;
		private BindingSource watchBinder;

		public MainWindow()
		{
			InitializeComponent();

			presenter = new Presenter_MainView(this);

			watchBinder = new BindingSource();
			dgWatch.DataSource = watchBinder;
		}

		public List<Watch>  WatchGridDataSource //Named "WatchGridDataSource", actually refers to BindingSource
		{
			set
			   {
				   foreach(Watch w in value)
				   {
					   watchBinder.Add(w);
				   }
				   watchBinder.ResetBindings(false);
			   }
		}
}



FolderSync.Presenters - Presenter_MainWindow
		private void checkModel(object state)
		{
			List<Watch> watches = syncEngine.Watches;
			List<Observation> observations = new List<Observation>();

			for (int i = 0; i < watches.Count; i++)
			{
				Watch w = watches[i];
				foreach (Observation obs in w.observations)
				{
					observations.Add(obs);
				}
			}

			UpdateGridData(watches, observations);
		}

		private void UpdateGridData(List<Watch> watchData, List<Observation> obsData)
		{
			if(watchData.Count > 0)
			{
				view.WatchGridDataSource = watchData;
			}
			if(obsData.Count > 0)
			{
				view.ObservationGridDataSource = obsData; 
			}
		}



Im not sure if i am using the binding source correctly but i am still getting the cross-thread InvalidOperation exception.

I have done alot of research on delegates and have got a solution using delegates to build, but the method in view is never called, dont know why. It's set up as follows;

firstly, I prepare the Mainwindow.cs by defining a method in the interface IMainView it conforms to which will be called by the presenter delegate;
FolderSync.Presenters - IMainView
public interface IMainView
{
	void bindWatchData(List<object> dataSource);
}




Then i create a delegate in the presenter to use whenever the datagridview in view needs updating.

FolderSync.Presenters - Presnter_MainWindow
public class Presenter_MainView
{
	delegate void Bind(List<object> dataSource);
	 
	public Presenter_MainView(IMainView view)
	{
		this.view = view;
	}

	private void UpdateGridData(List<Watch> watchData, List<Observation> obsData)
	{
		 if(watchData.Count > 0)
		 {
			 Bind watchBinder = new Bind(view.bindWatchData); // Not sure how to pass the 'watchData' through the delegate
		 }
		 if(obsData.Count > 0)
		 {
			  Bind watchBinder = new Bind(view.bindWatchData);
		 }
	 }




The real method this delegate is supposed to call in view is as follows;

FolderSync.View - Mainwindow.cs
public void  bindWatchData(List<object> dataSource)
{
	Type type = dataSource[0].GetType();

	if(type == typeof(Watch))
	{
			dgWatch.DataSource = dataSource;
	}
	if (type == typeof(Observation))
	{
			dgObservations.DataSource = dataSource;
	}
}




But this method never gets called, the delegate in presenter doesnt cause any build errors but doesnt call the method it is told to. Anyone with a more solid understanding of delegates see why ?
Was This Post Helpful? 0
  • +
  • -

#6 beatles1692   User is offline

  • D.I.C Head

Reputation: 13
  • View blog
  • Posts: 62
  • Joined: 03-December 08

Re: Executing .DataSource on the GUI thread

Posted 09 May 2009 - 08:50 PM

By this code:
private void UpdateGridData(List<Watch> watchData, List<Observation> obsData)
	{
		 if(watchData.Count > 0)
		 {
			 Bind watchBinder = new Bind(view.bindWatchData); // Not sure how to pass the 'watchData' through the delegate
		 }
		 if(obsData.Count > 0)
		 {
			  Bind watchBinder = new Bind(view.bindWatchData);
		 }
	 }


if that's where you want the binding happens you should invoke the delegates you have defined like this:
Bind watchBinder = new Bind(view.bindWatchData);
watchBinder(watchData);


I hope this will work.
as far as I know ,In case of a cross tread exception you should call your delegates using the invoke method of the object (control) that you'd like to update.
Take a look at this link.
Was This Post Helpful? 0
  • +
  • -

#7 Methical   User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 8
  • Joined: 08-May 09

Re: Executing .DataSource on the GUI thread

Posted 10 May 2009 - 06:18 AM

Ha HA ! Big thank you beatles. It worked !!!

For clarity and anyone else following / that may find this thread. My working project now works as follows.

Firstly, the interface that the Mainwindow.cs implements:
namespace FolderSync.Presenters
{
	public interface IMainView
	{
		void watchSource(List<Watch> source);
		void invokeWatchGrid(Delegate delegateMethod, IList data);

		void observationSource(List<Observation> source);
		void invokeObservationGrid(Delegate delegateMethod, IList data);
	}
}



This interface is implemented as follows within FolderSync.View.Mainwindow.cs

#region IMainView Members

		public void watchSource(List<Watch> source)
		{
			dgWatch.DataSource = source;
		}

		public void invokeWatchGrid(Delegate delegateMethod, System.Collections.IList data)
		{
			dgWatch.Invoke(delegateMethod, data);
		}

		public void observationSource(List<Observation> source)
		{
			dgObservations.DataSource = source;
		}

		public void invokeObservationGrid(Delegate delegateMethod, System.Collections.IList data)
		{
			dgObservations.Invoke(delegateMethod, data);
		}

		#endregion



So we can see that the 'view' has a method of binding a given List<Watch> or List<Observation> to the relevant DataGridView.DataSource property. To use this directly on another thread (such as the presenter thread) would cause the Cross-Thread exception so there much exist another method, on the same thread (GUI thread) which eventually calls this first method, binding the data to the control. Que, "invokeWatchGrid()" & "invokeObservationGrid()".

Now these invoke methods dont call the binding methods directly. Rather, the presenter, on a seperate thread calls these invoke methods. As these invoke methods do not directly access GUI controls, no exception is thrown. What the presenter does do however, is pass to the GUI thread an instance of a delegate method it would like the view to use to bind data to its controls. We can see this working below as;

namespace FolderSync.Presenters
{
	 public class Presenter_MainView
	{
		 public delegate void WatchBind(List<Watch> dataSource);
		 public delegate void ObservationBind(List<Observation> dataSource);
		 
		 WatchBind watchBind;
		 ObservationBind obsBind;

		 public Presenter_MainView(IMainView view)
		 {
			  watchBind = new WatchBind(view.watchSource);
			  obsBind = new WatchBind(view.observationSource);
		 }

		 private void UpdateGridData(List<Watch> watchData, List<Observation> obsData)
		{
			if(watchData.Count > 0)
			{
				view.invokeWatchGrid(watchBind, watchData);
			}

			if(obsData.Count > 0)
			{
				view.invokeObservationGrid(obsBind, obsData);
			}
		}
	}
}



So working as a whole;
  • Once the "UpdateGridData()" is called in the presenter, after initial checks to see if data actually exists, the presenter calls the "view.invokeWatchGrid()" or "view.invokeObservationGrid()" methods. Passed into these methods are the delegate methods owned by the presenter that it wants the view to use when binding its data along with the actual collection of data it wants binded to the datagridview control on the GUI.

  • As the invokeWatchGrid and invokeObservationGrid methods do not directly modify the GUI controls, they are allowed to be called from other threads! These methods in the Mainwindow.cs (view) then Invoke the delegate method that was given to it on the DataGridView on control in question. Note to use the second overload, Invoke(Delegate, params[]); so that you can pass the actual data you want to bind back into the presenters delegate methods.

  • Because the presenters delegates are called using the .Invoke from a GUI control, they are essentially called from the same thread the control was created on, these delegates then call the respective watchSource() or observationSource() methods in the view and bind the passed list to the control that invoked the delegate

*deep breath*

I think that's how it works anyway. I think the next thing to do would be to optimise this so that 2 delegates and 4 view methods are not required to get this to work. Im thinking maybe using generics to define the methods and then work out the types that have been passed in once inside the method. That way only 1 delegate method and 2 view methods would be needed to get this to run.

Once again, thank you for your help beatles1692, i thought i was going to be stuck on this for weeks !
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1