Asynchronous Programming Model - how to implement?

D

Dmitry Nogin

Hi,

I'm trying to implement APM (Asynchronous Programming Model) API by myself -
I mean:

IAsyncResult BeginMyFunc(AsyncCallback, object @object);
void EndMyFunc(IAsyncResult result);

functions pair. The problem is that my working thread has to report updates
to the GUI by System.Windows.Forms.Control.Invoke(Delegate method)
invocation.

How to implement my EndMyFunc(IAsyncResult result) function? It should be
something like this inside:

result.AsyncWaitHandle.WaitOne();

where AsyncWaitHandle is ManualResetEvent or something like this.
Unfortunately, this approach will prevent my GUI thread from handling
SendMessage calls coming from working thread invocations of
System.Windows.Forms.Control.Invoke(Delegate method). Deadlock
is about to happen.

The same problem is about terminating phase. I cannot just call this by GUI
thread:

workingThread.Join();

because of System.Windows.Forms.Control.Invoke(Delegate method) invocations
coming from the working thread. The deadlock again.

What I need is some kind of .NET replacement for MsgWaitForMultipleObjects +
some necessary message pumping. How to properly implement it in .NET 2.0?



many thanks in advance,

-- dmitry
 
D

Dmitry Nogin

Kevin, Thank you a lot!

but I really need another level of flexibility here. I need APM API where

void EndMyFunc(IAsyncResult result);

GUI thread call is blocking as it has to be. From other hand my working
thread should be able to SendMessage to GUI thread while it is blocked this
way. Are there _any_ chances to do it in .NET? I'm ready to write my own
WaitOne() method implementation - just tell me how to properly pump messages
in .NET, if possible. . .
 
B

Brian Gideon

Kevin, Thank you a lot!

but I really need another level of flexibility here. I need APM API where

void EndMyFunc(IAsyncResult result);

GUI thread call is blocking as it has to be. From other hand my working
thread should be able to SendMessage to GUI thread while it is blocked this
way. Are there _any_ chances to do it in .NET? I'm ready to write my own
WaitOne() method implementation - just tell me how to properly pump messages
in .NET, if possible. . .

Dmitry,

The WaitOne implementations in .NET *will* pump messages. Are you
observing a different behavior?

Brian
 
D

Dmitry Nogin

Brian Gideon said:
The WaitOne implementations in .NET *will* pump messages. Are you
observing a different behavior?


Dear Brian,

You can try to run the following code snippet (Visual C# Windows Application, VS2005):



using System;

using System.Windows.Forms;

using System.Threading;



namespace WindowsApplication5

{

public partial class Form1 : Form

{

public Form1()

{

InitializeComponent();

}



private void Form1_Load(object sender, EventArgs e)

{

ManualResetEvent mre = new ManualResetEvent(false);



Thread thread = new Thread((ThreadStart)delegate

{

Thread.Sleep(500);



Invoke((ThreadStart)delegate // blocks here

{

MessageBox.Show("Trying to send message...");

});



mre.Set();

});



thread.Start();

mre.WaitOne(); // and here

}

}

}



Please let me know if you would like me to explain this sample. . .



Thank you for your attention - I really appreciate it. :cool:
 
D

Dmitry Nogin

Sorry, I forgot to remove Thread.Sleep(500); - it means nothing...
Dmitry Nogin said:
The WaitOne implementations in .NET *will* pump messages. Are you
observing a different behavior?


Dear Brian,

You can try to run the following code snippet (Visual C# Windows Application, VS2005):



using System;

using System.Windows.Forms;

using System.Threading;



namespace WindowsApplication5

{

public partial class Form1 : Form

{

public Form1()

{

InitializeComponent();

}



private void Form1_Load(object sender, EventArgs e)

{

ManualResetEvent mre = new ManualResetEvent(false);



Thread thread = new Thread((ThreadStart)delegate

{

Thread.Sleep(500);



Invoke((ThreadStart)delegate // blocks here

{

MessageBox.Show("Trying to send message...");

});



mre.Set();

});



thread.Start();

mre.WaitOne(); // and here

}

}

}



Please let me know if you would like me to explain this sample. . .



Thank you for your attention - I really appreciate it. :cool:
 
B

Brian Gideon

Hi,

Okay, I may need to review this in a little more in detail myself.
That was a great example by the way. Replace your Form_Load code with
this:

private void Form1_Load(object sender, EventArgs e)
{
ManualResetEvent mre = new ManualResetEvent(false);

Thread thread = new Thread((ThreadStart)delegate
{
BeginInvoke((ThreadStart)delegate // blocks here
{
MessageBox.Show("Message");
});

MessageBox.Show("Before WaitHandle.Set()");

mre.Set();

MessageBox.Show("After WaitHandle.Set()");
});

thread.Start();

mre.WaitOne(); // and here

MessageBox.Show("Done");
}

Notice that I changed it to use BeginInvoke instead and I've added
message boxes to help visualize the order of what's going on. You'll
see that WaitOne does indeed pump messages, but it only does so
*after* Set is called. So it appears that WaitOne is alertable, but
only when signalled. Since your example was using Invoke the
WaitHandle never got signalled.

This was not what I was expecting. I'll have to do more research.

Brian
 
D

Dmitry Nogin

Hi,
I replaced WaitOne() to thread.Join() - we can see the same behaviour while
documentation states that Join function "Blocks the calling thread until a
thread terminates, while continuing to perform standard COM and SendMessage
pumping."


P.S. It's about my very basic scenario: to use PostMessage from GUI to
Working thread and SendMessage from working thread to GUI. Working thread
might be busy, so we need some kind of message queue and GUI thread should
react immediately and can return some user responses to the business logic
requests if necessary.

From one hand - I need asynchronous business object logic execution (on a
working thread) to be used in Windows Forms, etc. From another hand - I need
synchronous execution (on a GUI thread /or/ working thread + GUI thread
blocking) for job automation scripting, etc.

Everything is OK, but my legacy underlying API doesn't support multiple
threads, so I have to use working thread all the time + optional GUI thread
blocking. And I'd like my business objects to be databindable so I have to
raise all the events by my GUI thread.

Please, give me some tip! I'm about to change my development platform from
Win32 to .NET, so I read from side to side these 16 books in 6 months
(http://www.amazon.com/My-NET-readings/lm/R1E1A68BY7S8N0), but I'm still
very far from clear understanding of many .NET aspects. . . :cool:

Now I'm reading about "Multithreaded Programming with the Event-based
Asynchronous Pattern" but it looks like that my blocking problem is actual
anyway. . .
 
J

Jim Rand

So if I infer correctly
a.. you have an unmanaged DLL that does file transfer
b.. you need to set up these jobs in a .NET front-end to run immediately or at some scheduled time
c.. you need some kind of monitoring capability
Presumably you want these activities to run on separate background threads.

The reason for my curiosity is that your approach seems very complicated.

The approach I've used is to use the canned asynchronous threading that is part of .NET.

Here is some sample code you might find useful. In this case, the JobScheduler has no knowledge of the actual tasks performed. It only runs the JobDelegate. It does know how the job finished returned via the callback. Thread collisions are handled very simply via the _lockObj.

There is no explicit wait or joining of threads or any other complicated stuff.

To set up a job:

/* Start background job scheduler - give it a ISynchronizeInvokebto invoke methods on the GUI thread */
Helpers.LookupTableSingleton.Instance.dsLookupTbls.GUISync = (System.ComponentModel.ISynchronizeInvoke)this;

_jobScheduler = new AgencyInfo.Helpers.JobScheduler();
int interval = Int32.Parse(AppInfo.Settings("LookupTableRefresh"));
_jobScheduler.TaskAdd(new AgencyInfo.Helpers.JobSchedulerTask("LookupTableUpdate",
DateTime.Now + new TimeSpan(0, interval, 0), new TimeSpan(0, interval, 0),
Helpers.LookupTableSingleton.Instance.dsLookupTbls.UpdateLookupTables));
_jobScheduler.Start(new TimeSpan(0, 0, 30));

The delegate
----------------------------------------------------------------------------------------------------
/* Update lookup tables with new or changed records. */
internal Helpers.JobSchedulerTaskOutcome UpdateLookupTables()
{
bool ok = false;
bool hasNewData = false;

_dsNewData.Clear();

try
{
foreach (System.Data.SqlClient.SqlDataAdapter da in _adapterList)
{
string tableName = da.TableMappings[0].DataSetTable;

if (tableName != "Notes")
{
da.SelectCommand.Parameters["@MaxTS"].Value = _maxTSList[tableName];
}
da.Fill(_dsNewData, tableName);

if (_dsNewData.Tables[tableName].Rows.Count > 0) hasNewData = true;
}
}
catch (System.Data.SqlClient.SqlException ex)
{
_log.Error("SqlException UpdateLookupTables", ex);
}
catch (System.Exception ex)
{
_log.Error("Exception UpdateLookupTables", ex);
}

if ( hasNewData )
{
ok = UpdateDataSet();
}
else
{
ok = true;
}
return new AgencyInfo.Helpers.JobSchedulerTaskOutcome("LookupTableUpdate", ok);
}

/* The final data merging has to be done on the GUI thread */
private delegate bool UpdateDataSetDelegate();
private bool UpdateDataSet()
{
bool ok = false;
if (_GUISync.InvokeRequired)
{
ok = (bool) _GUISync.Invoke(new UpdateDataSetDelegate(UpdateDataSet), null);
}
else
{
try
{
this.AcceptChanges();
this.Merge(_dsNewData, false);
this.AcceptChanges();

NoteMaxTimestamps();

ok = true;

}
catch (System.Exception ex)
{
_log.Warn("UpdateDataSet merge failed", ex);
ok = false;
}
return ok;
}
return ok;
}


The Job Scheduler
----------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Runtime.Remoting.Messaging;

namespace AgencyInfo.Helpers
{
class JobScheduler
{
private static readonly log4net.ILog _log = log4net.LogManager.GetLogger(typeof(JobScheduler));
private object _lockObj = new object();
private Dictionary<string, JobSchedulerTask> _taskDictionary;
private TimeSpan _threadSleepTimeSpan;
private Thread _RunThread;

#region Constructors
public JobScheduler()
{
_taskDictionary = new Dictionary<string,JobSchedulerTask>();
}
#endregion

#region Public methods

public void Start(TimeSpan threadSleepTimeSpan)
{

_threadSleepTimeSpan = threadSleepTimeSpan;

_RunThread = new Thread(new ThreadStart(Run));
_RunThread.IsBackground = true;
_RunThread.Start();

}

public void Stop()
{
_RunThread.Abort();
}

public void TaskAdd(JobSchedulerTask task)
{
lock ( _lockObj )
{
if ( !_taskDictionary.ContainsKey(task.TaskName) )
{
_taskDictionary.Add(task.TaskName, task);
}
}
}

public void TaskRemove(string taskName)
{
lock (_lockObj)
{
if ( _taskDictionary.ContainsKey(taskName) )
{
_taskDictionary.Remove(taskName);
}
}
}

#endregion

#region ThreadStuff

private void Run()
{
try
{
while (true)
{
lock (_lockObj)
{
foreach (JobSchedulerTask task in _taskDictionary.Values)
{
if ((!task.IsBroken) && (!task.IsRunning) && (DateTime.Now >= task.StartAt))
{
task.IsRunning = true;
AsyncCallback callback = new AsyncCallback(JobFinished);
IAsyncResult ar = task.JobToRun.BeginInvoke(callback, null);
}
}
}

Thread.Sleep(_threadSleepTimeSpan);
}
}
catch (ThreadAbortException) { }
catch (Exception ex)
{
_log.Error("Exception in Run", ex);
}
finally
{
_taskDictionary.Clear();
_taskDictionary = null;
}

}

private void JobFinished(IAsyncResult ar)
{
JobSchedulerTask.JobDelegate jobDelegate = (JobSchedulerTask.JobDelegate)((AsyncResult)ar).AsyncDelegate;
JobSchedulerTaskOutcome outcome = jobDelegate.EndInvoke(ar);

lock (_lockObj)
{
if ( _taskDictionary.ContainsKey(outcome.TaskName) )
{
_taskDictionary[outcome.TaskName].IsRunning = false;
_taskDictionary[outcome.TaskName].IsBroken = !outcome.FinishedOK;
_taskDictionary[outcome.TaskName].StartAt = DateTime.Now + _taskDictionary[outcome.TaskName]._timeSpan;
}
}
}

#endregion

}

class JobSchedulerTask
{
public readonly TimeSpan _timeSpan;
public readonly string TaskName;
private DateTime _StartAt;
private bool _isRunning = false;
private bool _isBroken = false;
public readonly JobDelegate JobToRun;

public JobSchedulerTask(string taskName, DateTime startAt, TimeSpan timeSpan,
JobDelegate jobDelegate)
{
this.TaskName = taskName;
this._timeSpan = timeSpan;
this._StartAt = startAt;
this.JobToRun = jobDelegate;

}

public bool IsBroken
{
get { return _isBroken; }
set { _isBroken = value; }
}

public bool IsRunning
{
get { return _isRunning; }
set { _isRunning = value; }
}

public DateTime StartAt
{
get { return _StartAt; }
set { _StartAt = value; }
}

public delegate JobSchedulerTaskOutcome JobDelegate();

}

class JobSchedulerTaskOutcome
{
public readonly string TaskName;
public readonly bool FinishedOK;

public JobSchedulerTaskOutcome(string taskName, bool finishedOK)
{
this.TaskName = taskName;
this.FinishedOK = finishedOK;
}
}

}
 
D

Dmitry Nogin

Sorry for taking so long to get back to you. Please have a look at some
comments below.
So if I infer correctly
you have an unmanaged DLL that does file transfer
you need to set up these jobs in a .NET front-end to run immediately or at
some scheduled time
you need some kind of monitoring capability
Presumably you want these activities to run on separate background
threads.
The reason for my curiosity is that your approach seems very complicated.

I'd like to use Command design pattern to simplify implementation of
Undo/Redo in the GUI but we are talking about same approaches.

The approach I've used is to use the canned asynchronous threading that is
part of .NET.
Here is some sample code you might find useful. In this case, the
JobScheduler has no knowledge
of the actual tasks performed. It only runs the JobDelegate. It does know
how the job finished returned
via the callback. Thread collisions are handled very simply via the
_lockObj.
There is no explicit wait or joining of threads or any other complicated
stuff.

You don't need them because you are not going to expose synchronous version
of your API. I mean you have something like this:

void UpdateLookupTablesAsync()
{
_jobScheduler.TaskAdd(new
AgencyInfo.Helpers.JobSchedulerTask("LookupTableUpdate",
DateTime.Now + new TimeSpan(0, interval, 0),
new TimeSpan(0, interval, 0),
Helpers.LookupTableSingleton.Instance.dsLookupTbls.UpdateLookupTables));
}

but you don't expose synchronous version:

void UpdateLookupTables()

which blocks execution till the job completion.

Usually we need asynchronous versions to be used in WinForms, while
synchronous versions are suitable for batch files and WebForms. There are
some design patterns about:

http://msdn2.microsoft.com/en-us/library/ms228969.aspx

"The .NET Framework provides two design patterns for asynchronous
operations:
ž Asynchronous operations that use IAsyncResult objects.
ž Asynchronous operations that use events."

So, I have to say that .NET has a very, very serious multithreading problem.
 
D

Dmitry Nogin

I'm not so good in pinvoke (I've got no expirience at all :cool: - could
somebode be so kind to check this stuff? Now it works without blocking but
I'm not sure about possible consequences:


private void Form1_Load(object sender, EventArgs e)
{
//EventWaitHandle done = new ManualResetEvent(false);
EventWaitHandle done = new AlertableEvent(false,
EventResetMode.ManualReset);

Thread thread = new Thread((ThreadStart)delegate
{
Invoke((ThreadStart)delegate
{
MessageBox.Show("Trying to send message...");
});

done.Set();
});

thread.Start();
done.WaitOne();
}

// where AlertableEvent is done using MsgWaitForMultipleObjectsEx

class AlertableEvent : EventWaitHandle
{
public AlertableEvent(bool initialState, EventResetMode mode)
: base(initialState, mode)
{
}

public override bool WaitOne()
{
IntPtr[] handleArray = {this.Handle};
Message msg;

while (true)
switch(MsgWaitForMultipleObjectsEx(1, handleArray, INFINITE,
QueueStatusFlags.QS_ALLEVENTS, 0))
{
case WAIT_OBJECT_0: // event is signaled
return true;

case WAIT_OBJECT_0 + 1: // input is available
while (PeekMessage(out msg, IntPtr.Zero, 0, 0,
PeekMessageParams.PM_REMOVE))
DispatchMessage(ref msg);

continue;

default:
return false;
}
}

[Flags]
private enum QueueStatusFlags : uint
{
QS_KEY = 0x0001,
QS_MOUSEMOVE = 0x0002,
QS_MOUSEBUTTON = 0x0004,
QS_POSTMESSAGE = 0x0008,
QS_TIMER = 0x0010,
QS_PAINT = 0x0020,
QS_SENDMESSAGE = 0x0040,
QS_HOTKEY = 0x0080,
QS_ALLPOSTMESSAGE = 0x0100,
QS_RAWINPUT = 0x0400,
QS_MOUSE = QS_MOUSEMOVE | QS_MOUSEBUTTON,
QS_INPUT = QS_MOUSE | QS_KEY | QS_RAWINPUT,
QS_ALLEVENTS = QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT
| QS_HOTKEY,
QS_ALLINPUT = QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT
| QS_HOTKEY | QS_SENDMESSAGE
}

private const int WAIT_OBJECT_0 = 0;
private const uint INFINITE = 0xFFFFFFFF;

[DllImport("user32.dll")]
private static extern uint MsgWaitForMultipleObjectsEx(uint nCount,
IntPtr[] pHandles, uint dwMilliseconds, QueueStatusFlags dwWakeMask,
uint dwFlags);

[StructLayout(LayoutKind.Sequential)]
public struct Message
{
public IntPtr handle;
public uint msg;
public IntPtr wParam;
public IntPtr lParam;
public uint time;
public System.Drawing.Point p;
}

[Flags]
private enum PeekMessageParams : uint
{
PM_NOREMOVE = 0x0000,
PM_REMOVE = 0x0001,
PM_NOYIELD = 0x0002,
PM_QS_INPUT = QueueStatusFlags.QS_INPUT << 16,
PM_QS_POSTMESSAGE = (QueueStatusFlags.QS_POSTMESSAGE
| QueueStatusFlags.QS_HOTKEY | QueueStatusFlags.QS_TIMER) << 16,
PM_QS_PAINT = QueueStatusFlags.QS_PAINT << 16,
PM_QS_SENDMESSAGE = QueueStatusFlags.QS_SENDMESSAGE << 16
}

[SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("User32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool PeekMessage(out Message message,
IntPtr handle, uint filterMin, uint filterMax,
PeekMessageParams flags);

[DllImport("user32.dll")]
private static extern IntPtr DispatchMessage([In] ref Message lpmsg);
}
 
J

Jim Rand

Hi Dimitry,
You don't need them because you are not going to expose synchronous
version
of your API. I mean you have something like this:

void UpdateLookupTablesAsync()
{
_jobScheduler.TaskAdd(new
AgencyInfo.Helpers.JobSchedulerTask("LookupTableUpdate",
DateTime.Now + new TimeSpan(0, interval, 0),
new TimeSpan(0, interval, 0),

Helpers.LookupTableSingleton.Instance.dsLookupTbls.UpdateLookupTables));
}

but you don't expose synchronous version:

void UpdateLookupTables()

which blocks execution till the job completion.

Usually we need asynchronous versions to be used in WinForms, while
synchronous versions are suitable for batch files and WebForms. There are
some design patterns about:


How about this? - Part of the the data is fetched immediately and the rest
is fetched in background. Note the use of the ticket. If the tickets don't
match, the background fill is aborted.

------------------------------------------------------------------------------------
using System;
using System.Data;
using System.Threading;
using System.Runtime.Remoting.Messaging;
using log4net;

namespace JimRand.AgencyInfo.Datasets
{

partial class DSAgencyForm
{

private static readonly log4net.ILog _log =
log4net.LogManager.GetLogger(typeof(DSAgencyForm));
private
System.Collections.Generic.List<System.Data.SqlClient.SqlDataAdapter>
_adapterList = new
System.Collections.Generic.List<System.Data.SqlClient.SqlDataAdapter>();
private
System.Collections.Generic.List<System.Data.SqlClient.SqlDataAdapter>
_adapterListBkGrnd = new
System.Collections.Generic.List<System.Data.SqlClient.SqlDataAdapter>();
private
System.Collections.Generic.List<System.Data.SqlClient.SqlDataAdapter>
_adapterListForeGrnd = new
System.Collections.Generic.List<System.Data.SqlClient.SqlDataAdapter>();
private string _lastUpdatedBy;
private DataAdapters.DAAgencyForm _adapterSet;
private System.Data.DataSet _dsClone;
private int _ticket = 0;
private object _lockObj = new object();

/* Dataset must be updated on the GUI thread - see UpdateLookupTables() */
private System.ComponentModel.ISynchronizeInvoke _GUISync;

internal void Fill(int officeID)
{

lock (_lockObj)
{

_ticket++;

/* Set parameters for selected officeID */
_adapterSet.daNotes.SelectCommand.Parameters["@ParentTblID"].Value =
officeID;
_adapterSet.daOffice.SelectCommand.Parameters["@OfficeID"].Value =
officeID;
_adapterSet.daOfficeCnsltnt.SelectCommand.Parameters["@OfficeID"].Value
= officeID;
_adapterSet.daOfficeEnduserMM.SelectCommand.Parameters["@OfficeID"].Value
= officeID;
_adapterSet.daOfficeIdxUrl.SelectCommand.Parameters["@OfficeID"].Value =
officeID;
_adapterSet.daRefUrls.SelectCommand.Parameters["@OfficeID"].Value =
officeID;
_adapterSet.daUrlCnsltSolution.SelectCommand.Parameters["@OfficeID"].Value
= officeID;
_adapterSet.daUrlFraming.SelectCommand.Parameters["@OfficeID"].Value =
officeID;
_adapterSet.daUrlOfficeConsultant.SelectCommand.Parameters["@OfficeID"].Value
= officeID;
_adapterSet.daUrlOfficeEnduser.SelectCommand.Parameters["@OfficeID"].Value
= officeID;


/* Fill tables Office, Notes and OifficeIdxUrl on GUI thread */
Helpers.DataAccessLayer.Fill((System.Data.DataSet)this,
_adapterListForeGrnd);

/* Fill remaining tables on background thread */
BackGrndFillDelegate d = new BackGrndFillDelegate(BackGrndFill);
System.AsyncCallback ac = new
System.AsyncCallback(BackGrndFillCallback);
System.IAsyncResult ar = d.BeginInvoke(_ticket, ac, null);
}

} /* internal void Fill */

private delegate DatasetPackage BackGrndFillDelegate(int ticket);
private DatasetPackage BackGrndFill(int ticket)
{

/* Dataset to fill on the background thread */
System.Data.DataSet ds;
lock (_dsClone)
{
ds = _dsClone.Clone();
}

try
{
foreach (System.Data.SqlClient.SqlDataAdapter da in _adapterListBkGrnd)
{
lock (_lockObj)
{
if (ticket == _ticket)
{
string tableName = da.TableMappings[0].DataSetTable;
da.Fill(ds, tableName);
}
else
{
break;
}
}
}

}
catch (Exception ex)
{
_log.Error("BackGrndFill", ex);
}

return new DatasetPackage(ticket, ds);
}

private void BackGrndFillCallback(System.IAsyncResult ar)
{
BackGrndFillDelegate d =
(BackGrndFillDelegate)((AsyncResult)ar).AsyncDelegate;
DatasetPackage dsp = d.EndInvoke(ar);
MergeData(dsp);
}

private delegate void MergeDataDelegate(DatasetPackage dsp);
private void MergeData(DatasetPackage dsp)
{
if ( _GUISync.InvokeRequired )
{
_GUISync.Invoke(new MergeDataDelegate(MergeData), new object[] { dsp });
}
else
{
lock (_lockObj)
{
if (dsp.Ticket == _ticket)
{
/* Only merge if an update has not been started */
if (!this.HasChanges()) this.Merge(dsp.DS);
}

}
}
}

internal void
PrepareDatasetForUse(System.ComponentModel.ISynchronizeInvoke guiSync)
{

_GUISync = guiSync;

_adapterSet = new JimRand.AgencyInfo.DataAdapters.DAAgencyForm();

_lastUpdatedBy = Thread.CurrentPrincipal.Identity.Name;

/* All adapters - use during update */
_adapterList.Add(_adapterSet.daOffice);
_adapterList.Add(_adapterSet.daNotes);
_adapterList.Add(_adapterSet.daOfficeIdxUrl);
_adapterList.Add(_adapterSet.daUrlOfficeConsultant);
_adapterList.Add(_adapterSet.daUrlCnsltSolution);
_adapterList.Add(_adapterSet.daUrlFraming);
_adapterList.Add(_adapterSet.daUrlOfficeEnduser);
_adapterList.Add(_adapterSet.daRefUrls);
_adapterList.Add(_adapterSet.daOfficeCnsltnt);
_adapterList.Add(_adapterSet.daOfficeEnduserMM);

/* Foreground - fill immediate */
_adapterListForeGrnd.Add(_adapterSet.daOffice);
_adapterListForeGrnd.Add(_adapterSet.daNotes);
_adapterListForeGrnd.Add(_adapterSet.daOfficeIdxUrl);

/* Background - fill async */
_adapterListBkGrnd.Add(_adapterSet.daUrlOfficeConsultant);
_adapterListBkGrnd.Add(_adapterSet.daUrlCnsltSolution);
_adapterListBkGrnd.Add(_adapterSet.daUrlFraming);
_adapterListBkGrnd.Add(_adapterSet.daUrlOfficeEnduser);
_adapterListBkGrnd.Add(_adapterSet.daRefUrls);
_adapterListBkGrnd.Add(_adapterSet.daOfficeCnsltnt);
_adapterListBkGrnd.Add(_adapterSet.daOfficeEnduserMM);

Helpers.DataAccessLayer.FlipToProduction(_adapterList);

/* Model dataset for background filling */
_dsClone = this.Clone();
_dsClone.Relations.Clear();
_dsClone.Tables["Notes"].Constraints.Remove("FK_Office_Notes");
_dsClone.Tables["OfficeCnsltnt"].Constraints.Remove("FK_Office_OfficeCnsltnt");
_dsClone.Tables["OfficeEnduserMM"].Constraints.Remove("FK_Office_OfficeEnduserMM");
_dsClone.Tables["OfficeIdxUrl"].Constraints.Remove("FK_Office_OfficeIdxUrl");
_dsClone.Tables["UrlOfficeEnduser"].Constraints.Remove("FK_OfficeIdxUrl_UrlOfficeEnduser");
_dsClone.Tables["UrlFraming"].Constraints.Remove("FK_OfficeIdxUrl_UrlFraming");
_dsClone.Tables["UrlOfficeConsultant"].Constraints.Remove("FK_OfficeIdxUrl_UrlOfficeConsultant");
_dsClone.Tables["RefUrls"].Constraints.Remove("FK_OfficeIdxUrl_RefUrls");

_dsClone.Tables.Remove("Office");
_dsClone.Tables.Remove("Notes");
_dsClone.Tables.Remove("OfficeIdxUrl");

} /* internal void PrepareDatasetForUse */

internal void TearDown()
{
_adapterSet.Dispose();
_adapterList.Clear();
_adapterList = null;
_adapterListForeGrnd.Clear();
_adapterListForeGrnd = null;
_adapterListBkGrnd.Clear();
_adapterListBkGrnd = null;
} /* internal void TearDown */

/* Update all tables in the backend */
internal void Update()
{

/* Copy of changes used by DSLookupTbls.RefreshImmediate to determine
which tables to refresh immediatedly */
DataSet dsChanges = this.GetChanges();

Helpers.DataAccessLayer.Update((System.Data.DataSet)this, _adapterList,
true, _lastUpdatedBy);

/* Immediate refill of lookup tables impacted */
Helpers.LookupTableSingleton.Instance.dsLookupTbls.RefreshImmediate(dsChanges);

dsChanges.Dispose();

} /* internal void Update */

}

internal class DatasetPackage
{
public readonly System.Data.DataSet DS;
public readonly int Ticket;

internal DatasetPackage(int ticket, System.Data.DataSet ds)
{
this.Ticket = ticket;
this.DS = ds;
}
}

}
 
D

Dmitry Nogin

Jim Rand said:
How about this? - Part of the the data is fetched immediately and the rest
is fetched in background. Note the use of the ticket. If the tickets
don't match, the background fill is aborted.

Yep, it looks very natural. Unfortunately, my legacy underlying API allows
one thread only. I mean I cannot use two different threads interchangeably.
So I really need some alertable blocking in GUI thread while working one
does the entire job here.
 
D

Dmitry Nogin

BTW, there is very intresting blog somehow related to this topic:

"STAs, pumping, and the UI"
http://www.bluebytesoftware.com/blog/PermaLink,guid,6bdb2b54-a042-4eab-8cef-390603a58beb.aspx


Jim Rand said:
Hi Dimitry,
You don't need them because you are not going to expose synchronous
version
of your API. I mean you have something like this:

void UpdateLookupTablesAsync()
{
_jobScheduler.TaskAdd(new
AgencyInfo.Helpers.JobSchedulerTask("LookupTableUpdate",
DateTime.Now + new TimeSpan(0, interval, 0),
new TimeSpan(0, interval, 0),

Helpers.LookupTableSingleton.Instance.dsLookupTbls.UpdateLookupTables));
}

but you don't expose synchronous version:

void UpdateLookupTables()

which blocks execution till the job completion.

Usually we need asynchronous versions to be used in WinForms, while
synchronous versions are suitable for batch files and WebForms. There are
some design patterns about:


How about this? - Part of the the data is fetched immediately and the rest
is fetched in background. Note the use of the ticket. If the tickets
don't match, the background fill is aborted.

------------------------------------------------------------------------------------
using System;
using System.Data;
using System.Threading;
using System.Runtime.Remoting.Messaging;
using log4net;

namespace JimRand.AgencyInfo.Datasets
{

partial class DSAgencyForm
{

private static readonly log4net.ILog _log =
log4net.LogManager.GetLogger(typeof(DSAgencyForm));
private
System.Collections.Generic.List<System.Data.SqlClient.SqlDataAdapter>
_adapterList = new
System.Collections.Generic.List<System.Data.SqlClient.SqlDataAdapter>();
private
System.Collections.Generic.List<System.Data.SqlClient.SqlDataAdapter>
_adapterListBkGrnd = new
System.Collections.Generic.List<System.Data.SqlClient.SqlDataAdapter>();
private
System.Collections.Generic.List<System.Data.SqlClient.SqlDataAdapter>
_adapterListForeGrnd = new
System.Collections.Generic.List<System.Data.SqlClient.SqlDataAdapter>();
private string _lastUpdatedBy;
private DataAdapters.DAAgencyForm _adapterSet;
private System.Data.DataSet _dsClone;
private int _ticket = 0;
private object _lockObj = new object();

/* Dataset must be updated on the GUI thread - see UpdateLookupTables()
*/
private System.ComponentModel.ISynchronizeInvoke _GUISync;

internal void Fill(int officeID)
{

lock (_lockObj)
{

_ticket++;

/* Set parameters for selected officeID */
_adapterSet.daNotes.SelectCommand.Parameters["@ParentTblID"].Value =
officeID;
_adapterSet.daOffice.SelectCommand.Parameters["@OfficeID"].Value =
officeID;
_adapterSet.daOfficeCnsltnt.SelectCommand.Parameters["@OfficeID"].Value
= officeID;

_adapterSet.daOfficeEnduserMM.SelectCommand.Parameters["@OfficeID"].Value
= officeID;
_adapterSet.daOfficeIdxUrl.SelectCommand.Parameters["@OfficeID"].Value
= officeID;
_adapterSet.daRefUrls.SelectCommand.Parameters["@OfficeID"].Value =
officeID;

_adapterSet.daUrlCnsltSolution.SelectCommand.Parameters["@OfficeID"].Value
= officeID;
_adapterSet.daUrlFraming.SelectCommand.Parameters["@OfficeID"].Value =
officeID;

_adapterSet.daUrlOfficeConsultant.SelectCommand.Parameters["@OfficeID"].Value
= officeID;

_adapterSet.daUrlOfficeEnduser.SelectCommand.Parameters["@OfficeID"].Value
= officeID;


/* Fill tables Office, Notes and OifficeIdxUrl on GUI thread */
Helpers.DataAccessLayer.Fill((System.Data.DataSet)this,
_adapterListForeGrnd);

/* Fill remaining tables on background thread */
BackGrndFillDelegate d = new BackGrndFillDelegate(BackGrndFill);
System.AsyncCallback ac = new
System.AsyncCallback(BackGrndFillCallback);
System.IAsyncResult ar = d.BeginInvoke(_ticket, ac, null);
}

} /* internal void Fill */

private delegate DatasetPackage BackGrndFillDelegate(int ticket);
private DatasetPackage BackGrndFill(int ticket)
{

/* Dataset to fill on the background thread */
System.Data.DataSet ds;
lock (_dsClone)
{
ds = _dsClone.Clone();
}

try
{
foreach (System.Data.SqlClient.SqlDataAdapter da in _adapterListBkGrnd)
{
lock (_lockObj)
{
if (ticket == _ticket)
{
string tableName = da.TableMappings[0].DataSetTable;
da.Fill(ds, tableName);
}
else
{
break;
}
}
}

}
catch (Exception ex)
{
_log.Error("BackGrndFill", ex);
}

return new DatasetPackage(ticket, ds);
}

private void BackGrndFillCallback(System.IAsyncResult ar)
{
BackGrndFillDelegate d =
(BackGrndFillDelegate)((AsyncResult)ar).AsyncDelegate;
DatasetPackage dsp = d.EndInvoke(ar);
MergeData(dsp);
}

private delegate void MergeDataDelegate(DatasetPackage dsp);
private void MergeData(DatasetPackage dsp)
{
if ( _GUISync.InvokeRequired )
{
_GUISync.Invoke(new MergeDataDelegate(MergeData), new object[] {
dsp });
}
else
{
lock (_lockObj)
{
if (dsp.Ticket == _ticket)
{
/* Only merge if an update has not been started */
if (!this.HasChanges()) this.Merge(dsp.DS);
}

}
}
}

internal void
PrepareDatasetForUse(System.ComponentModel.ISynchronizeInvoke guiSync)
{

_GUISync = guiSync;

_adapterSet = new JimRand.AgencyInfo.DataAdapters.DAAgencyForm();

_lastUpdatedBy = Thread.CurrentPrincipal.Identity.Name;

/* All adapters - use during update */
_adapterList.Add(_adapterSet.daOffice);
_adapterList.Add(_adapterSet.daNotes);
_adapterList.Add(_adapterSet.daOfficeIdxUrl);
_adapterList.Add(_adapterSet.daUrlOfficeConsultant);
_adapterList.Add(_adapterSet.daUrlCnsltSolution);
_adapterList.Add(_adapterSet.daUrlFraming);
_adapterList.Add(_adapterSet.daUrlOfficeEnduser);
_adapterList.Add(_adapterSet.daRefUrls);
_adapterList.Add(_adapterSet.daOfficeCnsltnt);
_adapterList.Add(_adapterSet.daOfficeEnduserMM);

/* Foreground - fill immediate */
_adapterListForeGrnd.Add(_adapterSet.daOffice);
_adapterListForeGrnd.Add(_adapterSet.daNotes);
_adapterListForeGrnd.Add(_adapterSet.daOfficeIdxUrl);

/* Background - fill async */
_adapterListBkGrnd.Add(_adapterSet.daUrlOfficeConsultant);
_adapterListBkGrnd.Add(_adapterSet.daUrlCnsltSolution);
_adapterListBkGrnd.Add(_adapterSet.daUrlFraming);
_adapterListBkGrnd.Add(_adapterSet.daUrlOfficeEnduser);
_adapterListBkGrnd.Add(_adapterSet.daRefUrls);
_adapterListBkGrnd.Add(_adapterSet.daOfficeCnsltnt);
_adapterListBkGrnd.Add(_adapterSet.daOfficeEnduserMM);

Helpers.DataAccessLayer.FlipToProduction(_adapterList);

/* Model dataset for background filling */
_dsClone = this.Clone();
_dsClone.Relations.Clear();
_dsClone.Tables["Notes"].Constraints.Remove("FK_Office_Notes");

_dsClone.Tables["OfficeCnsltnt"].Constraints.Remove("FK_Office_OfficeCnsltnt");

_dsClone.Tables["OfficeEnduserMM"].Constraints.Remove("FK_Office_OfficeEnduserMM");

_dsClone.Tables["OfficeIdxUrl"].Constraints.Remove("FK_Office_OfficeIdxUrl");

_dsClone.Tables["UrlOfficeEnduser"].Constraints.Remove("FK_OfficeIdxUrl_UrlOfficeEnduser");

_dsClone.Tables["UrlFraming"].Constraints.Remove("FK_OfficeIdxUrl_UrlFraming");

_dsClone.Tables["UrlOfficeConsultant"].Constraints.Remove("FK_OfficeIdxUrl_UrlOfficeConsultant");

_dsClone.Tables["RefUrls"].Constraints.Remove("FK_OfficeIdxUrl_RefUrls");

_dsClone.Tables.Remove("Office");
_dsClone.Tables.Remove("Notes");
_dsClone.Tables.Remove("OfficeIdxUrl");

} /* internal void PrepareDatasetForUse */

internal void TearDown()
{
_adapterSet.Dispose();
_adapterList.Clear();
_adapterList = null;
_adapterListForeGrnd.Clear();
_adapterListForeGrnd = null;
_adapterListBkGrnd.Clear();
_adapterListBkGrnd = null;
} /* internal void TearDown */

/* Update all tables in the backend */
internal void Update()
{

/* Copy of changes used by DSLookupTbls.RefreshImmediate to determine
which tables to refresh immediatedly */
DataSet dsChanges = this.GetChanges();

Helpers.DataAccessLayer.Update((System.Data.DataSet)this, _adapterList,
true, _lastUpdatedBy);

/* Immediate refill of lookup tables impacted */

Helpers.LookupTableSingleton.Instance.dsLookupTbls.RefreshImmediate(dsChanges);

dsChanges.Dispose();

} /* internal void Update */

}

internal class DatasetPackage
{
public readonly System.Data.DataSet DS;
public readonly int Ticket;

internal DatasetPackage(int ticket, System.Data.DataSet ds)
{
this.Ticket = ticket;
this.DS = ds;
}
}

}
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top