I was asking about specifics with this approach earlier today here but the confused responses has me wondering if I'm going about this all wrong.
So here's the problem. I need to architect a WinForms application that periodically checks a value from some external service. If it is in a certain state the application is supposed to pop up a modal form blocking the user from interacting with the application until such time that the state changes. The blocked UI should act just like when a form is displayed using ShowDialog(). That is to say the user should not be able to interact with the ui in any way until the dialog is closed (in this case this can only be done by the application).
In my particular application I have the requirement crop up in several instances. For example, at one point the application runs a timer that repeatedly checks the value of a cell in a db table that is constantly being updated by a weighing device. If the value is below a certain threshold the user is prompted to place items on the scale - he or she may not proceed nor interact with the UI until they do so.
Every solution I have tried has been chock full of threading problems. Either the dialog won't block the UI, or the timer will not continue checking the external service or the application cannot close the form when the time comes.
This seems like a fairly standard winforms requirement and there must be a relevant design pattern that does not bring in Windows.Forms.Timer (I like keep it my views passive). How would you do this?
-
You might want to block the current thread until an other thread do the check in the background. This can be done by joining (
.Join()
) the thread that do the check in the background. This on will block the calling thread until it's over. If I have more time I'll make a snippet code for you, but this might give you an idea.Here is the code (Form1 and add 2 buttons : Button1 and Button2).
public partial class Form1 : Form { Random r = new Random(); public Form1() { InitializeComponent(); } Thread tCheck; private void Form1_Load(object sender, EventArgs e) { tCheck = new Thread(new ThreadStart(checkMethod)); } private void button1_Click(object sender, EventArgs e) { this.Text = r.Next(0, 10).ToString(); if (tCheck.ThreadState == ThreadState.Stopped) { this.button1.Text = "Stopped"; } } // checks a value from some external service HERE private void checkMethod() { int i =0; while(i<20) { Thread.Sleep(200); i++; } } private void button2_Click(object sender, EventArgs e) { tCheck.Start(); Thread.Sleep(500); tCheck.Join();//blocking the user from interacting with the application until such time that the state changes } }
Click button 2 and the thread will block all until the method with check for external value (this is done for this example will the loop).
George Mauer : this is not the same though since you're triggering the blocking activity by clicking a button the current thread you're joinig with is the UI thread. But in the example where you have a monitor the joining thread would be the thread for a timer callback. It wouldn't block at all. -
The previous post (with the Join()) blocks the UI but also prevents it from rendering, showing a blank (white) window, like if the app crashed. Better option would be to create a custom modal window (that the user can't move or close or do anything) with an event listener that will close the window in its handler. Then as soon as your weighing device detects a weight it can fire the event (or somehow communicate with your app, webservices, ipc, etc) to notify it.
George Mauer : ok...but how to block the user from just clicking around the window and keep typing in text underneath it? -
Finally figured out a solution that feels natural and works great. The trick was finally reading the Albahari threading ebook and learning about ManualResetEvent.
Here's how it works. You declare an instance of ManualResetEvent on your class and when you reach the point that your application should block at call resetEvent.WaitOne(timeout). This blocks the thread until another thread (which is checking some condition calls resetEvent.Set(). Here is my class in full.
/// <summary> /// If necessary, resolve the tare for a given transaction by querying the measurement /// device and comparing with a threshold. Will block until an appropriate tare is provided /// or a timeout condition is met. /// </summary> public class TareResolver { private IDevice _device; private IDialogFactory _dialogs; private ManualResetEvent _resolvedEvent = new ManualResetEvent(false); ICurrentDateTimeFactory _nowFactory; ICheckInSettings _settings; ITimer _timer; private UnitOfMeasureQuantityPair _threshold; public TareResolver(IDevice device, IDialogFactory dialogs, ICheckInSettings settings, ICurrentDateTimeFactory nowFactory) { _device = device; _dialogs = dialogs; _settings = settings; _nowFactory = nowFactory; _timer = new DriverInterface2.Domain.Utilities.Timer(_settings.TarePollFreqeuency); _timer.Tick += new Action(_timer_Tick); } void _timer_Tick() { if (ThresholdMet(_device.CurrentValue, _threshold)) _resolvedEvent.Set(); } public virtual UnitOfMeasureQuantityPair Resolve(Transaction transaction) { _threshold = _settings.TareThreshold; if (!ThresholdMet(_device.CurrentValue, _threshold)) { var dialog = _dialogs.GetProvideTareDialog(); dialog.Show(); _timer.Start(); if (!_resolvedEvent.WaitOne(_settings.TakeTareTimeout, false)) { _timer.Stop(); dialog.Hide(); throw new TimeoutException("Tare provider operation has either been cancelled or has timed out"); } _timer.Stop(); dialog.Hide(); } return _device.CurrentValue; } private bool ThresholdMet(UnitOfMeasureQuantityPair val, UnitOfMeasureQuantityPair thresh) { if ((thresh == null) || (val >= thresh)) return true; return false; } }
0 comments:
Post a Comment